introdu˘c~ao a programa˘c~ao funcional · hugs (haskell users gofer system) que tem algumas...

189
Introdu¸c˜ ao ` aPrograma¸c˜ ao Funcional Jos´ e Bernardo Barros Departamento de Inform´ atica da Universidade do Minho Campus de Gualtar — Braga — Portugal

Upload: others

Post on 26-Jun-2020

2 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

Introducao a Programacao Funcional

Jose Bernardo Barros

Departamento de Informatica da Universidade do MinhoCampus de Gualtar — Braga — Portugal

Page 2: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

Autores: Jose Bernardo BarrosTıtulo: Introducao a Programacao Funcional

Page 3: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

Indice

Prefacio 1

Capıtulo 1. A linguagem Haskell 3

Capıtulo 2. Programas simples 15

Capıtulo 3. Funcoes recursivas 31

Capıtulo 4. Listas 41

Capıtulo 5. Ordenacao de listas 55

Capıtulo 6. Funcoes de ordem superior 65

Capıtulo 7. Travessia de listas 75

Capıtulo 8. Representacao de dados 83

Capıtulo 9. Tipos indutivos 93

Capıtulo 10. Classes e polimorfismo 105

Capıtulo 11. Interaccao 115

Capıtulo 12. Modulos 125

Capıtulo 13. Arvores binarias de procura 129

Capıtulo 14. Funcoes finitas 143

Capıtulo 15. Grafos 149

Capıtulo 16. Reconhecimento de texto 163

Capıtulo 17. Computacao com estado 171

Glossario 183

iii

Page 4: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

Contacto dos autores:

Jose Bernardo BarrosDepartamento de Informatica, Universidade do MinhoCampus de Gualtar4710-057 BragaPortugalemail: [email protected]: www.di.uminho.pt/~jbb/

Page 5: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

Prefacio

Este livro aborda primordialmente a escrita de programas de computadores. Umprograma pode ser visto como uma maquina que transforma informacao: a materiaprima dessas maquinas chamam-se as entradas (ou inputs) enquanto que ao produtofinal se chamam as saıdas (ou outputs).Existem duas grandes classes de linguagens de programacao, i.e., linguagens de escritade programas: linguagens imperativas e declarativas.Numa linguagem de programacao imperativa, descreve-se a forma de obter osoutputs a partir dos inputs. Nesta classe encontram-se algumas das mais utilizadaslinguagens de programacao, tais como C ou Java.Em termos praticos, estas linguagens tem como grande vantagem o facto de os pro-gramas nela escritos estarem mais perto (a nıvel de modelo abstracto) das maquinasfısicas a que chamamos computadores. Isto deve-se sobretudo a acasos da historia masnao deve, de forma alguma, ser menosprezado. Como consequencia desta proximidadede modelos abstractos, e muito mais facil obter tradutores de programas escritos nestaslinguagens para a linguagem que e entendida pelo computador.Mas nao ha so vantagens na programacao imperativa. Uma das grandes deficienciasdeste estilo de programacao e ser normalmente muito difıcil determinar a relacaoprecisa entre as entradas e as saıdas de um programa. E isto e uma caracterısticaimportante quando pretendemos raciocinar sobre os programas. Um engenheiro civil,ao projectar uma ponte, tem ao seu dispor, mecanismos formais que lhe permitemprovar que a ponte nao caira desde que sejam salvaguardadas algumas condicoes deutilizacao. Com o crescente custo das tarefas que hoje em dia sao da responsabilidadedos computadores, torna-se imperativo que o responsavel pela escrita de um dessesprogramas possa ter mecanismos semelhantes para raciocinar sobre esses programas.Um programa escrito numa linguagem de programacao declarativa descreve arelacao existente entre os inputs e os outputs, sem duidar da forma como tais outputssao obtidos. Esta descricao pode ser sob a forma de predicados (como acontece naprogramacao logica) ou pode ser sob a forma de equacoes (como acontece na pro-gramacao funcional). E por isso mais facil raciocinar sobre programas escritos nestaslinguagens.

1

Page 6: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

2 PREFACIO

Este livro e um livro de introducao a programacao de computadores. O paradigmafuncional e, aqui, a escolha natural: dada a sua simplicidade sintactica, dada a clarezados seus conceitos semanticos e dada a universalidade computacional desses conceitos.Simultaneamente, atraves do paradigma funcional, pretende-se que o leitor veja facil-itado o seu primeiro contacto com o problema da programacao de computadores e,eventualmente, com o proprio computador.A escolha da linguagem Haskell para suporte a este curso e resultado de variosfactores.Em primeiro lugar escolhemos a linguagem Haskell porque ela incorpora todasas componentes das linguagens funcionais mais recentes: a computacao retardada(lazy evaluation), que permite fazer computacoes com estruturas infinitas, a modular-idade que permite desenvolver projectos de grandes dimensoes, um sistema de tipospolimorfico bem incorporado na linguagem e a programacao por monads que permitelibertar a linguagem de todos os aspectos que nao sejam puramente funcionais.Em segundo lugar escolhemos esta linguagem porque, ao contrario de outros sistemasfuncionais que sao extremamente exigentes em termos de recursos computacionais esao frequentemente solucoes proprietarias (que exigem licencas pagas), o Haskelltem implementacoes de domınio publico muito pouco exigentes em recursos computa-cionais. Isto permite ao leitor deste livro dispor de uma implementacao da linguagemcompletamente livre de encargos e capaz de ser executada num simples computadorpessoal.

Este livro esta organizado em duas partes. A primeira parte, correspondente aoscapıtulos 1 a 12, foca varios aspectos da programacao funcional e corresponde aonucleo basico de uma disciplina de introducao a programacao (funcional). A segundaparte, composta pelos capıtulos 13 a 17, apresenta alguns problemas classicos daprogramacao.

Page 7: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

CAPITULO 1

A linguagem Haskell

Nos dias de hoje e rara a pessoa que nunca tenha usado uma maquina de calcular.Nestes dispositivos podemos encontrar varias caracterısticas de um interpretador deuma linguagem de programacao. Atentemos por exemplo nas varias teclas que existemnuma tal maquina. E natural agrupa-las em tres categorias, de acordo com as suasfuncoes

• Um primeiro grupo de teclas que sao usadas para introduzir os numeros.Estas teclas tem como funcao a introducao dos dados do problema a resolver.• O segundo grupo de teclas e usado para indicarmos quais as operacoes que

pretendemos efectuar sobre os dados.• O terceiro grupo e constituıdo unicamente pela tecla = . O efeito desta

tecla e a de, como que por um passo de magia, obter o resultado pretendido.

Tambem neste livro sobre programacao funcional faremos uma distincao clara entretres grandes grupos de conceitos.

Dados. Para alem dos valores numericos disponıveis numa simples maquina decalcular, vamos poder usar outros tipos de valores tais como caracteres ou texto.Vamos ainda apresentar uma linguagem rigorosa e precisa para descrever os dadosdos problemas a resolver.

Operacoes. Tal como numa maquina de calcular existe de um vasto numero deoperacoes para manipular os dados. E no entanto neste grupo que as diferencas comuma maquina de calcular se acentuarao. E tudo isto porque temos a possibilidade dedefinir (i.e., programar) novas operacoes a partir de outras ja existentes.

Calculo. Ao conhecermos o processo de calculo usado pelo interpretador, podemosfazer com que os calculos sejam efectuados de uma forma mais rapida e eficiente.A linguagem que vamos usar para escrever os nossos programas chama-se Haskell1.Esta linguagem foi desenhada por um grupo de investigadores liderado por PhilipWadler na Universidade de Glasgow na Escocia. A descricao desta linguagem e umdocumento conhecido por Haskell Report e existem varias interpretadores (i.e.,programas que executam os programas escritos em Haskell) desta linguagem. Nestecurso vamos referir o uso de dois destes interpretadores.

1Homenagem ao matematico Haskell Curry cujo trabalho foi crucial para o desenvolvimento dasciencias da computacao em geral e da programacao funcional em particular.

3

Page 8: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

4 1. A LINGUAGEM HASKELL

• ghci (Glasgow Haskell Compiler interactive) que e talvez a implementacaomais usada e completa da linguagem.• hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de

vista didatico.

Existe uma vasta literatura sobre estas implementacao e existem ainda versoes paravarias plataformas (MacOS X, Unix, Linux e Windows). Uma boa fonte de informacaosobre a linguagem Haskell, assim como sobre a instalacao destas varias versoes, podeser obtida atraves da world-wide-web em

http://www.haskell.org

Ao invocarmos o ghci aparece-nos a seguinte mensagem:___ ___ _

/ _ \ /\ /\/ __(_)

/ /_\// /_/ / / | | GHC Interactive, version 6.0, for Haskell 98.

/ /_\\/ __ / /___| | http://www.haskell.org/ghc/

\____/\/ /_/\____/|_| Type :? for help.

Loading package base ... linking ... done.

Prelude>

Nesta altura o ghci esta pronto para responder as nossas perguntas. Podemos, porexemplo, calcular quantos minutos tem um dia.

Prelude> 60 * 24

1440

Prelude>

Apos efectuar o calculo, volta a aparecer Prelude> indicando que podemos continuara fazer perguntas (ao texto Prelude> e costume chamar-se o prompt do sistema).Para calcular a soma dos numeros inteiros de 1 a 10 podemos escrever

Prelude> 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10

55

Prelude>

Uma outra forma de efectuar este calculo seriaPrelude> sum [1..10]

55

Prelude>

Este calculo, em vez de usar a operacao de adicao de dois numeros, usa a funcao sum;esta funcao aplica-se a uma lista de numeros e calcula a soma dos elementos dessalista. A expressao [1..10] representa a lista dos numeros inteiros entre 1 e 10. Umaforma alternativa de a escrever e [1,2,3,4,5,6,7,8,9,10].Para alem destes calculos podemos invocar varios comandos do ghci. Estes sao facil-mente identificados pois os seus nomes comecam por dois pontos (:). Talvez os maisimportantes nesta altura sejam

• :q que termina a execucao do ghci

Page 9: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

1. A LINGUAGEM HASKELL 5

• :? que da uma lista de todos os comandos existentes

Se em vez de termos usado o ghci tivessemos usado o hugs a unica diferenca teriasido na fase de arranque do programa. Assim no inıcio terıamos obtido:

__ __ __ __ ____ ___ _________________________________________

|| || || || || || ||__ Hugs 98: Based on the Haskell 98 standard

||___|| ||__|| ||__|| __|| Copyright (c) 1994-2002

||---|| ___|| World Wide Web: http://haskell.org/hugs

|| || Report bugs to: [email protected]

|| || Version: November 2002 _________________________________________

Haskell 98 mode: Restart with command line option -98 to enable extensions

Reading file "/usr/local/lib/hugs/lib/Prelude.hs":

Hugs session for:

/usr/local/lib/hugs/lib/Prelude.hs

Type :? for help

Prelude>

Tudo o resto seria exactamente igual ao que vimos no ghci. Por este motivo, soexplicitaremos o interpretador que estamos a usar quando isso for importante para otopico em causa.

A utilizacao das varias operacoes disponıveis esta sujeita a uma certa disciplina. Porexemplo, nao faz sentido somar duas listas de numeros nem aplicar a funcao sum a umnumero. Esta disciplina, em ciencias da computacao chama-se disciplina de tipos.Cada entidade tem a si associado um determinado tipo que delimita o conjunto deaccoes que podem ser executadas sobre essa entidade.Para dizermos que uma expressao E tem o tipo T (ou ainda, que a expressao E habitao tipo T ) vamos usar a notacao E :: T .Convem fazer aqui uma nota sobre esta notacao. Na maioria da literatura sobre tiposem ciencias da computacao a relacao de habitabilidade e representada pelo sımbolo“:” (dois pontos). Aqui usaremos o sımbolo “::” porque e este o sımbolo utilizado emHaskell. Como veremos mais a frente, nesta linguagem os dois pontos sao utilizadosnoutro contexto.A principal propriedade desta disciplina de tipos e que entidades iguais tem omesmo tipo2.

2O conceito de tipo tem um paralelo muito conhecido na Fısica e que corresponde as dimensoesdas grandezas fısicas. No Sistema Internacional (SI) existem 3 dimensoes basicas: massa (M), com-

primento (L) e tempo (T ). A velocidade, por exemplo, que mede a variacao da posicao (L) porunidade de tempo (T ) tem dimensoes LT−1. A aceleracao, por sua vez, medindo a variacao de

velocidade por unidade de tempo, tem dimensoes LT−2. A unidade de forca no SI e o Newton e as

suas dimensoes sao MLT−2. Podemos ver que estas dimensoes sao coerentes com a segunda lei de

Newton (F = ma): as dimensoes em ambos os membros da equacao coincidem. E claro que nem

Page 10: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

6 1. A LINGUAGEM HASKELL

O primeiro grupo de tipos de que falaremos e o dos tipos primitivos ou atomicos.Estes sao aqueles que nao dependem de mais nada e que por isso mesmo sao a basepara a construcao de todos os outros. Os tipos primitivos existentes em Haskell sao

• Int• Integer• Float• Double• Char• Bool

Os quatro primeiros tipos sao os tipos dos valores numericos. Nos dois primeiros casotemos numeros inteiros (como, por exemplo, 3 ou -45). A diferenca entre estes doistipos consiste na gama de definicao destes numeros. Um elemento do tipo Integer naotem qualquer limite (por isso se diz tratar-se de numeros inteiros de precisao ilimitada).No caso dos elementos do tipo Int, a sua gama esta limitada entre um limite inferior(designado por minBound) e por um limite superior (designado por maxBound).Podemos usar o interpretador para saber os valores destes valores na implementacaousada.

Prelude> minBound :: Int

-2147483648

Prelude> maxBound :: Int

2147483647

Prelude>

Algumas das operacoes que podemos efectuar sobre numeros inteiros sao:

• adicao (+), subtraccao (-), multiplicacao (*) e exponenciacao (^)• divisao inteira (div) e resto da divisao inteira (mod).

Por exemplo,Prelude> (2 * 3) ˆ (5 - 3)

36

Prelude> div 52 9

5

Prelude> mod 52 9

7

Prelude>

Estes exemplos mostram que alguns dos sımbolos de operacao existentes em Haskellsao infixos (isto e, o sımbolo aparece entre os operandos) – e o caso das operacoes doprimeiro grupo – enquanto que outros sao prefixos (isto e, o sımbolo aparece antes dosoperandos) – e o caso das operacoes do segundo grupo.Um sımbolo infixo pode ser usado como prefixo se tivermos o cuidado de o englobarpor parentesis. Por exemplo,

todas as equacoes em que ambos os membros tenham a mesma dimensao sao correctas; no entanto,este e um primeiro criterio de correccao de uma qualquer lei fısica.

Page 11: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

1. A LINGUAGEM HASKELL 7

Prelude> (*) 2 3

6

Prelude> (+) 43 34

77

Prelude>

De uma forma analoga, um sımbolo de operacao prefixo pode ser usado como infixo,se tivermos o cuidado de o englobar pelos caracteres “‘”. Por exemplo,

Prelude> 52 ‘div‘ 9

5

Prelude> 52 ‘mod‘ 9

7

Prelude>

O segundo grupo de tipos numericos e o dos numeros de vırgula flutuante (como, porexemplo, 3.75 ou -3.1415). Tambem aqui, a existencia de dois tipos para represen-tar estes numeros prende-se com a precisao pretendida. As operacoes que podemosefectuar sobre elementos deste tipo incluem, entre outras, adicao (+), subtraccao (-),multiplicacao (*), e divisao (/).O terceiro tipo referido acima e o dos caracteres. Exemplos de elementos deste tiposao ’a’, ’9’, ’!’.Veremos mais adiante que estes serao os constituintes atomicos das palavras e textos.O ultimo dos tipos elementares referidos acima e o dos valores logicos (True e False).Das operacoes possıveis sobre elementos deste tipo destacamos:

• o e logico ou conjuncao, denotado por && (operador infixo),• o ou logico ou disjuncao, denotado por || (operador infixo),• a negacao, denotada por not.

Veremos mais a frente outras operacoes que podem ser efectuadas com elementosdestes tipos.

Uma das vantagens do sistema de tipos e a possibilidade de definir tipos mais com-plexos a partir de outros mais simples. Para isso, em Haskell existem dois constru-tores fundamentais de tipos: o produto cartesiano e as funcoes.

• O tipo (A1, A2, . . . , An) tem habitantes com a forma (a1, a2, . . . , an) ondea1 :: A1, a2 :: A2, . . . an :: An. No caso particular do produto cartesiano dedois tipos, existem as funcoes fst e snd para seleccionar as componentes deum par.

Prelude> fst (3,56)

3

Prelude> snd (3,(5,6))

(5,6)

• Dados os tipos A e B, o tipo A -> B e constituıdo por funcoes cujo argumentoe do tipo A e o resultado e do tipo B. Uma funcao pode ser vista como umaforma de transformar os argumentos num resultado. Em Haskell existe

Page 12: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

8 1. A LINGUAGEM HASKELL

uma notacao de escrita de funcoes que se baseia neste ponto de vista. Porexemplo,

– para descrever a funcao que retorna o seu argumento multiplicado por3, escrevemos \x -> x * 3

– para descrever a funcao que recebe um par e retorna um outro com aordem dos seus elementos trocados, escrevemos \(x,y) -> (y,x)

Esta notacao e conhecida como notacao-λ3.Prelude> (\x -> x * 3) 4

12

Prelude> (\(x,y) -> (y,x)) (3,(5,6))

((5,6),3)

Nesta altura, o leitor provavelmente perguntar-se-a se cada entidade tem sempre as-sociado a si um tipo e se nao podera ter mais do que um. As regras sintacticas deconstrucao das varias entidades sao feitas de tal forma que cada entidade tem sempreassociado a si um tipo.No entanto a resposta a segunda questao depende fundamentalmente da disciplina detipos adoptada. Em algumas linguagens de programacao esta disciplina impoe quecada entidade tenha a si associado um unico tipo. Em Haskell contudo, a disci-plina de tipos e mais poderosa, permitindo que a uma mesma entidade possam estarassociados mais do que um tipo. Esta caracterıstica e normalmente conhecida porpolimorfismo e, mesmo assim, pode ser mais ou menos restritiva. Uma das for-mas de polimorfismo existente em Haskell provem da possibilidade de se definirementidades parametrizadas. Destas, sem duvida a mais paradigmatica e a funcao iden-tidade, isto e, a funcao definida por

id x = x

Para descrever o tipo desta funcao vamos precisar de usar tipos que podem sersubstituıdos por outros tipos, isto e, aquilo a que chamaremos tipos variaveis ouvariaveis de tipo.O tipo da funcao identidade e escrito como id :: α -> α.Veremos mais a frente, e com bastante detalhe, mais pormenores sobre a disciplinade tipos existente em Haskell, bem como conceitos que se aplicam tambem a outraslinguagens de programacao. No entanto, e para terminar esta pequena introducao adisciplina de tipos existente em Haskell, falta-nos referir um processo conhecido porcurrying (de novo uma alusao ao matematico Haskell Curry). Para isso, perguntemo-nos qual sera o tipo da funcao + que retorna a soma de dois numeros inteiros. A

3A origem desta notacao esta relacionada com um equıvoco entre o matematico Haskell Curry

e um tipografo. Conta-se que Curry, para descrever a funcao que tinha argumento x e resultado f xcostumava escrever bx.f x. Ao enviar um texto com esta notacao para a tipografia, dactilografou ^x.f

x. O tipografo, por sua vez interpretou tal notacao de uma forma diferente, e aquilo que apareceu

impresso foi λx.f x. Por razoes que se prendem com a configuracao dos teclados dos computadores,esta forma de descrever funcoes em Haskell sofreu mais uma adulteracao, passando a (\ x -> f x).

Page 13: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

1. A LINGUAGEM HASKELL 9

resposta mais imediata e a de dizer que esta funcao aceita como argumento um parde numeros inteiros e da como resultado um numero inteiro; isto e,

+ :: (Int,Int) -> Int

Operacionalmente esta caracterizacao pode ser vista como: para usarmos a funcao +devemos fornecer-lhe simultaneamente dois numeros. Uma caracterizacao alternativaseria, em vez de fornecer os dois numeros simultaneamente, fornece-los um a um; emtermos de tipos teremos entao

+′ :: Int -> (Int -> Int)

Note-se que existe uma diferenca subtil entre estas caracterizacoes: so podemos falarno resultado de + depois de lhe fornecermos os seus dois argumentos; por exem-plo +(3, 5). No entanto, +′ da um resultado logo que lhe seja fornecido o primeironumero. Qual sera entao o resultado de +′ 3? Uma pista para respondermos a estapergunta pode ser obtida atraves da determinacao do tipo da expressao +′ 3. Usandoas definicoes acima podemos entao concluir que +′ 3 e uma funcao que tem um argu-mento do tipo Int e cujo resultado tambem e do tipo Int. E que funcao sera esta? Aunica forma de podermos concluir que as funcoes + e +′ sao realmente a mesma (e porisso + (3,5) = (+′ 3) 5) e admitindo que +′ 3 e a funcao que da como resultado ovalor do seu argumento somado a 3.Este raciocınio pode ser generalizado para funcoes com qualquer numero de argumen-tos: se a funcao f tem tipo

f :: (T1, T2, . . . , Tn)→ T

entao podemos escrever uma funcao fcurry do tipo

fcurry :: T1 → (T2 → . . .→ (Tn → T ) . . .)

de tal forma que

f(x1, x2, . . . , xn) = (. . . ((fcurryx1)x2) . . .)xn

A funcao fcurry e costume chamar-se a versao curry de f .Por este processo ser muito utilizado, e costume utilizar a seguinte convencao deparentesis:

• em vez de escrevermos

f :: T1 → (T2 → (· · · → (Tn → T ) . . .))

vamos escrever

f :: T1 → T2 → · · · → Tn → T

• em vez de escrevermos

(. . . ((fcurryx1)x2) . . .)xn

vamos escreverfcurry x1 x2 . . . xn

Page 14: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

10 1. A LINGUAGEM HASKELL

Aos programas em Haskell chamam-se modulos. Estes sao compostos por umconjunto de definicoes de tipos e de funcoes.Estas definicoes devem ser colocadas num ficheiro. Estes, por sua vez, sao normalmentereferidos por scripts, e por convencao, tem extensao .hs (de haskell script).Para alem disso o texto dos programas deve ter indicacao do nome do modulo emcausa. Usa-se para isso a sıntaxe

module NomedoModulo where

A criacao destes ficheiros tem de ser feita atraves do uso de um editor de texto.Suponhamos que existe um destes programas no ficheiro quadrado.hs.

quadrado.hsmodule Quadrado where

quad x = x * xcubo x = x * (quad x)

Como ja referimos, apos a invocacao do ghci, aparece no ecra um texto que terminacom uma linha da forma

Prelude>

Nesta altura podemos comecar a efectuar calculos. Por exemplo,Prelude> 3*3

9

Prelude> quad 3

<interactive>:1: Variable not in scope: ‘quad’

Prelude>

O calculo acima deu um erro como resultado. Isto porque ainda nao demos qualquerinstruccao que nos permita usar as definicoes do dito ficheiro. Para isso, devemos emprimeiro lugar instruir o ghci para ler esse ficheiro. O comando apropriado para issoe :load quadrado.hs e o resultado obtido e o seguinte.

Prelude> :load quadrado

Compiling Quadrado ( quadrado.hs, interpreted )

Ok, modules loaded: Quadrado.

*Quadrado>

Note-se que o mesmo efeito seria obtido se fizessemos apenas :load quadrado em vezde :load quadrado.hs, ja que estamos a usar o prefixo standard para as scripts ghci.Alem disso, o comando :load pode ser abreviado para :l, e por isso, a forma maiseconomica de efectuar esta operacao seria :l quadrado.A partir deste momento podemos usar, para alem de todas as funcoes disponıveis noghci (definicoes essas que se encontram no ficheiro Prelude.hs), as funcoes definidasno ficheiro quadrado.hs.

Page 15: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

1. A LINGUAGEM HASKELL 11

Uma outra consequencia desta operacao foi a mudanca do prompt, que passou a ser*Quadrado>.Podemos entao experimentar as definicoes acima.

*Quadrado> quad 3

9

*Quadrado> (quad 3) + (cubo 3)

36

*Quadrado> 64

*Quadrado>

O comando

*Quadrado> :set +s

*Quadrado>

faz com que, no final de cada calculo nos seja mostrada uma indicacao do esforco com-putacional associado a esse calculo. Vejamos agora o que acontece quando voltamosa repetir os calculos anteriores.

*Quadrado> quad 3

9

(0.00 secs, 0 bytes)

*Quadrado> (quad 3) + (cubo 3)

36

(0.01 secs, 0 bytes)

*Quadrado> cubo (quad 2)

64

(0.01 secs, 0 bytes)

*Quadrado>

Como podemos ver, e-nos indicado o tempo que foi usado bem como a memorianecessaria a efectuar os calculos.No hugs, a informacao que e mostrada com esta opcao e ligeiramente diferente –aparece o numero de reducoes efectuadas bem como o numero de celulas de memoriausadas no calculo.

Quadrado> quad 3

9

(17 reductions, 50 cells)

Quadrado> (quad 3) + (cubo 3)

36

(25 reductions, 37 cells)

Quadrado> cubo (quad 2)

64

(21 reductions, 33 cells)

Quadrado>

Existem ainda outros comandos deste tipo (para alem dos ja mencionados :quit e:load).

Page 16: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

12 1. A LINGUAGEM HASKELL

O comando :set acima referido, tem inumeras opcoes. Uma que tem particular inter-esse e a que faz com que seja mostrado o tipo de cada uma das expressoes calculadas.Vejamos qual o seu efeito.

*Quadrado> :set +t

*Quadrado> quad 3

9

cmTypeOfName: it

it :: Integer

*Quadrado> (quad 3) + (cubo 3)

36

cmTypeOfName: it

it :: Integer

*Quadrado> cubo (quad 2)

64

cmTypeOfName: it

it :: Integer

Para anular o efeito desta opcao devemos usar o comando :unset +t.Tambem aqui, o hugs tem um comportamento ligeiramente diferente.

Quadrado> :set +t

Quadrado> quad 3

9 :: Integer

Quadrado> (quad 3) + (cubo 3)

36 :: Integer

Quadrado> cubo (quad 2)

64 :: Integer

Quadrado>

Para, no hugs, anular o efeito da operacao set +t devemos invocar o comando set -t.

Exercıcios

1.1 Sabendo que

odd :: Int -> Boolnot :: Bool -> BooltoInteger :: Float -> Integermod :: Int -> Int -> Int

indique (caso exista) o tipo de cada um dos seguintes termos:(1) (odd 3,not True, False)(2) mod 10(3) not (toInt 3.0)(4) not (odd (toInt 0.5))(5) (3.5, toInt, odd)(6) (mod 10, 3.5)

1.2 Indique um termo de cada um dos seguintes tipos:

Page 17: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

1. A LINGUAGEM HASKELL 13

(1) (Integer,Integer)(2) (Integer->Bool,Integer)(3) (Integer,(Bool,Integer))(4) (Integer,Bool,Integer)

1.3 Partindo do princıpio de que

f :: Integer → (Bool → Integer)g :: (Bool → Integer) → (Bool → [Integer])h :: Integer → ([Bool] → Integer)

(1) Calcule (caso existam) os tipos dos seguintes termos:(a) f 3(b) g (f 4)(c) (g (f 4)) True(d) g (f ((h 3) [True,False])) True

(2) Para cada um dos termos da alınea anterior, elimine os parentesis desnecessarios.(3) Indique qual o tipo de x de forma a que sejam bem formados os seguintes

termos:(a) h ((f 3) True) x(b) (g x) True(c) f((h 3) x)

Page 18: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao
Page 19: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

CAPITULO 2

Programas simples

Aquilo que apresentamos ate aqui nao se afasta muito de uma potente maquina de cal-cular. A grande diferenca entre um interpretador de uma linguagem de programacaofuncional e uma maquina de calcular reside precisamente na capacidade de progra-mar, isto e, de construir novas funcoes a partir de outras ja existentes.Para fazer estas novas definicoes vamos usar equacoes que relacionam o valor dosargumentos das funcoes com os seus resultados. Por exemplo, para definirmos umafuncao quad :: Int -> Int, que da como resultado o quadrado do seu argumento,podemos usar a seguinte equacao

quad x = x * x

Esta equacao tem dois papeis bem distintos:

• por um lado, define qual a relacao existente entre o argumento da funcao eo seu resultado;• pelo outro lado, da-nos uma “receita” ou metodo de obter o resultado da

funcao quando conhecemos o valor do seu argumento.

Ao primeiro papel representado por esta equacao chamamos especificacao; ao se-gundo chamamos programa.Note-se que, sempre que o segundo papel esta presente numa equacao, o primeirotambem o esta. O inverso pode nao acontecer. Atente-se na seguinte equacao.

(raizQ x) * (raizQ x) = x

Embora esta equacao defina uma relacao bem precisa entre o resultado da funcaoraizQ e o valor do seu argumento, nao nos fornece nenhum processo de calculo doresultado.O processo de calculo usado na programacao funcional esta fortemente relacionadocom a manipulacao algebrica a que todos nos habituamos no ensino secundario e quenessa altura chamavamos simplificacao. Do ponto de vista das ciencias da computacao,esta manipulacao algebrica e vulgarmente conhecida por reducao ou reescrita. De umaforma simplista podemos descreve-lo com substituir instancias de lados esquerdos deequacoes pelas correspondentes instancias dos lados direitos.Para podermos descrever este processo de uma forma minimamente rigorosa temos queintroduzir alguns conceitos basicos da manipulacao de expressoes simbolicas, nomeada-mente os conceitos de termo (e arvore associada a um termo), substituicao e instancia.

15

Page 20: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

16 2. PROGRAMAS SIMPLES

As expressoes que usamos no dia-a-dia, quer em linguagem natural quer em Matematica,obedecem a regras de boa formacao ou sintacticas. De certa forma e-nos facil recon-hecer que as expressoes

• roeu rato a corda o• 5 ∗+34

estao mal formadas, ao contrario das expressoes

• o rato roeu a corda• 5 + (3 + 4)

Nas expressoes matematicas, em particular, existem regras de boa formacao bemprecisas. E comum nestas expressoes identificar os varios sımbolos de operacao (taiscomo os sımbolos ‘+’ e ‘∗’) e identificar as (sub-) expressoes a que essas operacoes saoaplicadas. Daqui resulta uma visao hierarquica das expressoes matematicas a que ecostume designar por arvore associada a expressao (cf. com as arvores genealogicas).Por exemplo, na expressao 5 ∗ (3 + 4) podemos identificar a operacao ∗ aplicada aduas sub-expressoes. Enquanto que a primeira destas nao se pode decompor mais, nasegunda podemos identificar a operacao + aplicada a duas sub-expressoes. O resultadodesta analise e a seguinte arvore.

5 +

2

*

4

Note-se que, nesta representacao, a utilizacao dos parentesis deixa de ser necessaria.Na representacao habitual das expressoes os parentesis sao usados para agrupar ex-pressoes complexas numa unica.Transpondo a nomenclatura de arvores para este contexto, o sımbolo mais exteriorde uma expressao e costume designar-se por raız da arvore, enquanto que as folhascorrespondem aos operandos atomicos, i.e., que nao se podem decompor noutros maissimples.Usando esta forma de visualizar expressoes, as regras de boa formacao que uma ex-pressao tem de satisfazer podem ser descritas atraves da descricao do numero e tipodos operandos de cada operacao.Nos exemplos de expressoes que temos vindo a apresentar, as folhas das arvores cor-respondem sempre a constantes (numericas). Para podermos escrever programas ra-zoavelmente complexos vamos precisar tambem de usar variaveis. Em termos de regrasde boa formacao, uma variavel pode aparecer onde as constantes aparecem, i.e., como

Page 21: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

2. PROGRAMAS SIMPLES 17

folhas das arvores. Ha no entanto uma mecanismo associado a manipulacao de ex-pressoes onde o papel das variaveis e determinante. Este mecanismo e conhecidopor substituicao. Veremos mais a frente o papel importante que este mecanismodesempenha na execucao dos programas.Recordemos aqui que uma equacao e composta por duas expressoes – os dois lados(ou membros) da equacao. Tendo isto em consideracao, uma equacao de definicao deuma funcao f deve satisfazer os seguintes requisitos:

• o sımbolo mais externo do primeiro membro da equacao tem de ser f;• no primeiro membro da equacao nao deve haver variaveis repetidas;• todas as variaveis que ocorrem no segundo membro da equacao tambem

devem aparecer no primeiro membro dessa mesma equacao.

Para melhor compreender estes requisitos, vamos dar exemplos de equacoes que osnao satisfazem.

• Para definir a funcao max que recebe como argumento um par de numerosinteiros e da como resultado o maior deles poderıamos escrever as equacoes.

(max (x,y) >= x) = True(max (x,y) >= y) = True

Estas equacoes nao safisfazem dois dos requisitos acima mencionados:(1) o sımbolo mais exterior dos lados esquerdos das equacoes e o sımbolo

>= e nao aquele que estamos a tentar caracterizar (i.e., max) e,(2) do lado esquerdo das equacoes aparecem variaveis repetidas (na primeira,

a variavel x e na segunda a variavel y).• Para definir a funcao iguais que recebe como argumento um par de numeros

e da como resultado verdadeiro quando esses numeros sao iguais e falsoquando sao diferentes, poderıamos escrever a equacao.

iguais (x,x) = True

No entanto nesta equacao a variavel x aparece repetida no lado esquerdo.Note-se que esta equacao satisfaz a primeira das regras enunciadas: o sımbolomais exterior do lado esquerdo e de facto o sımbolo da operacao que estamosa definir (iguais).

Para conseguirmos descrever a execucao (calculo) dos programas funcionais temosque comecar por definir o mecanismo de substituicao. Dadas expressoes e e s e umavariavel x, representamos por e[x := s] a expressao que resulta de substituir todas asocorrencias de x em e por s. Por exemplo, (a ∗ (3 + a))[a := (y + 1)] e a expressao(y + 1) ∗ (3 + (y + 1)).

Page 22: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

18 2. PROGRAMAS SIMPLES

* 3 + ( )

* 3 + ( )

(y+1)

(y+1)(y+1)

aa

Este mecanismo pode ainda ser generalizado de forma a uma substituicao poder actuarsobre mais do que uma variavel. Por exemplo, ((x+ y) ∗ x)[x := 3, y := a+ 1] resultana expressao (3 + (a+ 1)) ∗ 3.

y

(a + 1)

yx x

x

3

(a + 1)3 3( )+ *

( )+ *

A nocao de instancia e definida a custa do mecanismo de substituicao. Dadas duasexpressoes e e p dizemos que e e uma instancia de p se existe uma substituicao queaplicada a p resulta em e. Por exemplo, a expressao (y + 1) ∗ (3 + (y + 1)) e umainstancia de (a ∗ (3 + a)). Como vimos, a substituicao [a := (y + 1)] transforma(a ∗ (3 + a)) em (y + 1) ∗ (3 + (y + 1)).Note-se que esta relacao de instancia nao e, modo geral, simetrica. O leitor poderaser tentado a pensar que o efeito da substituicao [a := (y + 1)] pode ser anulado pelasubstituicao [(y + 1) := a]; mas esta ultima nao e realmente uma substituicao. So asvariaveis podem ser substituıdas!Estamos agora em condicoes de analisar em mais pormenor o processo de simplificacaoreferido acima.Suponhamos que querıamos calcular, usando a equacao (i.e., o programa) da pagina 15

Page 23: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

2. PROGRAMAS SIMPLES 19

quad (quad 3)

Nesta expressao existem duas instancias do lado esquerdo da equacao que define afuncao quad:

• a substituicao [x := 3] aplicada a quad x resulta na expressao quad 3 que euma sub-expressao de quad (quad 3)• a substituicao [x := quad 3] aplicada a quad x resulta na expressao quad(quad 3) que e uma sub-expressao de quad (quad 3)

A cada uma destas expressoes chama-se um redex (de reducible Expression). Dizemosentao que a expressao quad (quad 3) tem dois redexes.Cada passo de reducao corresponde a substituir uma destas sub-expressoes (que comovimos sao instancias do lado esquerdo de uma equacao) pela correspondente instanciado lado direito da mesma equacao. Esta expressao que vai substituir o redex e nor-malmente chamada contractum.Como existem dois redexes, o primeiro passo no calculo quad (quad 3) pode ser feitode duas formas diferentes:

• quad (quad 3) =⇒ quad (3 ∗ 3) = quad 9• quad (quad 3) =⇒ (quad 3) ∗ (quad 3)

E claro que, cada uma das expressoes obtidas pode ser novamente reduzida, poiscontem redexes. As varias opcoes de calculo desta expressao podem ser vistas noseguinte diagrama:

quad (quad 3)

(quad 3) * (quad 3)

(3 * 3) * (3 * 3)

quad (3 * 3)

(3 * 3) * (quad 3) (quad 3) * (3 * 3)

O facto de todas estas estrategias de calculo (ou reducao) terem o mesmo resultadonao e uma simples coincidencia. Sempre que as equacoes escritas obedecam as regrasenunciadas acima a ordem pela qual as reducoes sao feitas nao e relevante para o resul-tado obtido. No entanto existe uma ordem bem determinada pela qual estas reducoessao feitas. Esta ordem, ou estrategia de reducao, e uma caracterıstica importante decada linguagem de programacao.No caso da linguagem que vamos usar neste curso, a estrategia de reducao usada econhecida por lazy evaluation – o que traduzido a letra significa calculo preguicoso.

Page 24: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

20 2. PROGRAMAS SIMPLES

Um outro nome, mais elucidativo, desta estrategia e outermost, leftmost (i.e., o maisexterior e mais a esquerda). Muitas vezes esta estrategia e ainda denominada porretardada ou nao estrita.Vejamos entao o que este termo significa e porque e que ele descreve uma estrategiade reducao.Tal como foi visto atras, as expressoes podem ser vistas como arvores: a expressao5 + (2 ∗ 4) corresponde a arvore

5 +

2

*

4

A cada nodo de uma arvore fica naturalmente associada uma “profundidade”. Naarvore acima o nodo 2 esta a uma maior profundidade maior do que o nodo +. Poroutras palavras, o nodo + e mais externo do que o nodo 2. Ja a profundidade dosnodos 4 e 2 e igual. O nodo 2 esta mais a esquerda.Este exemplo da-nos uma ideia de qual o significado do termo outermost, leftmost nadescricao de uma estrategia de reducao – esta vai tentar reduzir o termo que esta maisexterior, ou em caso de igualdade de profundidade, o termo mais a esquerda.Vejamos entao novamente o exemplo da pagina 19. Para reduzirmos a expressao

quad (quad 3)

podıamos ter escolhido reduzir para quad (3 ∗ 3) ou (quad 3) ∗ (quad 3).A arvore correspondente a expressao a ser reduzida poe em evidencia que estas reducoesocorrem a profundidades diferentes.

quad

quad

3

Page 25: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

2. PROGRAMAS SIMPLES 21

A primeira alternativa corresponde a uma posicao mais profunda (i.e., menos exterior).Daı que, em Haskell fosse a segunda, a estrategia adoptada.Vejamos entao um outro passo do mesmo exemplo. A expressao

(quad 3) * (quad 3)

pode ser reduzida para (3*3) * (quad 3) ou (quad 3) * (3 * 3)

Neste caso, a expressao a ser reduzida e representada pela arvore

*

quad

3

quad

3

Ambas as possibilidades envolvem reducoes a mesma profundidade. Por isso, emHaskell vai ser adoptada a estrategia que envolve a reducao feita mais a esquerda,isto e, a primeira das hipoteses apresentadas.Consideremos agora a funcao const definida pela seguinte equacao.

const x y = x

Vejamos entao qual a estrategia seguida para calcular const 3 (2 / 0). Sob a formade uma arvore, esta expressao e representada por

const

3 /

2 0

Assim, a reducao que vai ser feita em primeiro lugar resulta imediatamente em 3.Este exemplo explica qual a razao de a estrategia descrita ser tambem conhecida porlazy-evaluation ou nao estrita.

• Esta estrategia corresponde a nao calcular o valor dos argumentos de umafuncao enquanto tal calculo puder ser adiado. Parodiando um proverbio

Page 26: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

22 2. PROGRAMAS SIMPLES

popular, podemos dizer que a maxima associada a esta estrategia e naofacas hoje o que podes nao ter que fazer amanha;• Uma funcao diz-se estrita quando da um valor indefinido sempre que os seus

argumentos estao indefinidos; podemos ver que algumas funcoes definidas emHaskell sao nao-estritas – apesar de um dos argumentos que fornecemos afuncao const ser indefinido (2/0), a funcao “retornou” um valor definido.

Para finalizar esta seccao introdutoria, vamos definir algumas funcoes muito simplesque nos permitirao tambem apresentar muita da sintaxe especıfica do Haskell.Para iniciar este nosso estudo, vamos tentar resolver alguns problemas geometricosmuito simples. Em primeiro lugar vamos definir uma funcao que calcule a distanciaentre dois pontos de um plano. Um ponto, pode ser representado pelas suas coorde-nadas cartesianas. Para isso podemos fazer a seguinte definicao.

type Ponto = (Float,Float)

Esta definicao introduz uma abreviatura – Ponto – para o tipo

(Float,Float)

Tal como referimos acerca dos nomes dos tipos, tambem as abreviaturas de tiposdevem comecar por uma letra maiuscula.Para calcular a distancia entre os pontos A = (xA, yA) e B = (xB , yB), a formula ausar e.

AB =√

(xA − xB)2 + (yA − yB)2

Para definirmos a funcao que calcula a distancia entre dois pontos vamos precisar deusar as funcoes fst e snd que calculam o valor da primeira e segunda componentesde um par. Por exemplo

Prelude> fst (3.4,2.5)

3.4

Prelude> snd (3.4,2.5)

2.5

Prelude> fst (3.4,(4.5,’a’))

3.4

Prelude> snd (3.4,(4.5,’a’))

(4.5,’a’)

Prelude>

Alem destas, vamos tambem precisar da funcao de calculo da raız quadrada – sqrt, eda potencia – ^.Com estes dados todos podemos definir a funcao distancia como

distancia (a,b)= sqrt (((fst a)-(fst b))ˆ2+((snd a)-(snd b))ˆ2)

Page 27: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

2. PROGRAMAS SIMPLES 23

O lado direito desta equacao e difıcil de ler, nao so porque temos que usar as funcoesfst e snd para poder manipular as componentes do par (a,b), mas tambem porqueisso obriga a utilizar muitos parentesis.Podemos tornar esta definicao muito mais legıvel usando um formalismo conhecidopor pattern-matching ou concordancia de padroes.Na definicao acima, podemos constatar que os dois argumentos da funcao distanciasao pares, isto e, da forma (x,y). Incorporando esta informacao no lado esquerdo daequacao, temos

distancia ((xA,yA), (xB,yB)) = sqrt ((xA-xB)ˆ2+(yA-yB)ˆ2)

A versao curried (cf. pagina 8) desta funcao e:

distancia :: Ponto -> Ponto -> Floatdistancia (xA,yA) (xB,yB) = sqrt ((xA-xB)ˆ2+(yA-yB)ˆ2)

De agora em diante, vamos optar por escrever todas as nossas definicoes desta forma(curried). As vantagens desta opcao serao evidentes mais a frente.Analisemos agora com algum pormenor esta ultima definicao.

• A primeira linha explicita o tipo da funcao que estamos a definir. Emboranao seja obrigatorio fazer esta explicitacao (na maior parte dos casos, ointerpretador e capaz de inferir o seu tipo a partir da definicao da funcao),ela ajuda a escrita e compreensao da definicao. Uma outra forma de escrevero tipo desta funcao seria nao recorrer a definicao da abreviatura Ponto, i.e.,escreve-la como

distancia :: (Float,Float) -> (Float,Float) -> Float

• A segunda linha refere-se a definicao propriamente dita e corresponde quasedirectamente a formula apresentada acima. Do ponto de vista meramentesintactico podemos reparar que tanto o nome da funcao como o das variaveiscomecam com uma letra minuscula. Esta e uma regra (sem excepcoes) emHaskell.

Uma construcao muito util nas linguagens funcionais e a construcao let...in....Usando esta construcao, a definicao anterior poderia ser escrita como

distancia (xA,yA) (xB,yB) =let dx = xA-xB

dy = yA-yBin sqrt (dxˆ2 + dyˆ2)

Page 28: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

24 2. PROGRAMAS SIMPLES

Esta construcao permite-nos dividir as definicoes em varios blocos. Esta divisao econseguida pela definicao de nomes locais para sub-expressoes. No exemplo dado, saodefinidas dois novos nomes dx e dy que sao depois usados na definicao do resultadoda funcao em causa.E claro que esta fragmentacao da definicao pode ser levada mais ou menos longe. Umadefinicao equivalente a estas duas ultimas seria

distancia (xA,yA) (xB,yB) =let dx = xA-xB

dy = yA-yBd = dxˆ2 + dyˆ2

in sqrt d

Um outro mecanismo de estruturacao das definicoes e o uso da construcao ...where....A definicao acima poderia ser escrita como:

distancia (xA,yA) (xB,yB) =sqrt d whered = dxˆ2 + dyˆ2dx = xA-xBdy = yA-yB

Note-se que, tanto numa como na outra forma de estruturar as definicoes, a ordem pelaqual os blocos aparecem nao e relevante. Por isso, uma outra definicao equivalente e:

distancia (xA,yA) (xB,yB) =let d = dxˆ2 + dyˆ2

dx = xA-xBdy = yA-yB

in sqrt d

Veremos mais a frente algumas diferencas entre estes dois mecanismos.

Um aspecto bastante peculiar da sintaxe do Haskell e o significado da identacao dotexto. Em Haskell, ao contrario de muitas outras linguagens de programacao (C,Modula-2, Java, ...), a forma como o texto de uma definicao esta disposto, tem umsignificado bem preciso.As regras fundamentais para compreender o significado da identacao sao:

(1) Se uma linha comeca mais a frente do que comecou a linha anterior, entao eladeve ser considerada uma continuacao da linha anterior. Assim por exemplo,escrever

Page 29: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

2. PROGRAMAS SIMPLES 25

quadrado x= x * x

e equivalente a escrever

quadrado x = x * x

(2) Se uma linha comeca na mesma coluna que a anterior, entao elas sao consid-eradas definicoes independentes.

(3) Se uma linha comeca mais atras do que a anterior, entao essa linha naopertence a mesma lista de definicoes.

Embora estas regras parecam um pouco confusas, a pratica mostra que o seu uso setorna bastante facil e natural. Uma maxima que muitas vezes ajuda a decidir quala identacao correcta e que definicoes pertencentes a uma mesma lista de definicoesdevem comecar na mesma coluna.

Um cırculo pode ser representado usando o seu centro (um ponto) e o raio (distancia).As abreviaturas de tipos que se seguem expressam exactamente essa ideia:

type Circulo = (Centro, Raio)type Centro = Pontotype Raio = Float

E de notar que as abreviaturas de tipos tem que comecar por uma letra maiuscula;como veremos mais a frente, esta regra aplica-se ainda a outras situacoes.Uma funcao que teste se um ponto esta ou nao dentro de um cırculo deve dar uma deduas respostas possıveis:

• verdadeiro (True) – se a distancia desse ponto ao centro for menor ou igualao raio, ou• falso (False) – no outro caso.

O tipo do resultado dessa funcao deve entao ser Bool.

dentro :: Ponto -> Circulo -> Booldentro p (c,r) =if ((distancia p c) <= r)then Trueelse False

De novo podemos notar que o tipo desta funcao poderia ser escrito sem recorrer asabreviaturas definidas:

dentro :: (Float,Float) -> ((Float,Float),Float) -> Float

Esta forma e, sem duvida alguma, muito menos compreensıvel.

Page 30: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

26 2. PROGRAMAS SIMPLES

Quanto a definicao da funcao, ela contem uma estrutura de controlo muito utilizada– a estrutura if...then...else. O significado desta construcao pode ser descritopelas seguintes equacoes:

if True then a else b = aif False then a else b = b

Vejamos entao como e que e calculado (i.e., reduzido) o valor da expressao

dentro (1, 1) ((0, 0),3)

Em cada passo da reducao vamos realcar a sub-expressao que esta a ser reduzida.dentro (1, 1) ((0, 0),3)

=⇒if ((distancia (1, 1) (0, 0)) <= 3) then True else False

=⇒if (sqrt ((1-0)ˆ2+(1-0)ˆ2) <= 3) then True else False

=⇒if (1.41421 <= 3) then True else False

=⇒if True then True else False

=⇒True

As regras apresentadas acima para descreverem o comportamento da estrutura condi-cional sao suficientes para mostrar as seguintes igualdades.

• if b then True else False = b• if b then False else True = not b• if b then b’ else False = b && b’• if b then True else b’ = b || b’

O leitor mais incredulo pode fazer uma tabela de verdade para mostrar que estasigualdades sao realmente validas.Com estas igualdades podemos apresentar uma outra versao para a definicao da funcaodentro.

dentro p (c,r) = ((distancia p c) <= r)

A estrutura de controlo if...then...else... e usada para expressar alternativas.Existem outras formas sintacticas para este fim. Por exemplo, a definicao acimapoderia ser escrita da forma

dentro p (c,r)| ((distancia p c) <= r) = True| ((distancia p c) > r) = False

Esta forma de escrever alternativas esta mais de acordo com a notacao matematicausual.

dentro p (c,r) ={

True se (distancia p c) <= rFalse se (distancia p c) > r

Page 31: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

2. PROGRAMAS SIMPLES 27

Note-se ainda que a segunda condicao nao e nada mais do que a negacao da primeira.Podemos, por isso mesmo, usar uma forma alternativa – otherwise – para expressareste facto. A definicao ficara entao:

dentro p (c,r)| ((distancia p c) <= r) = True| otherwise = False

Ja referimos uma outra forma de definir funcoes – por concordancia de padroes – queconsiste em incorporar no lado esquerdo das equacoes, a informacao acerca da formados argumentos. Por exemplo, para definirmos a funcao que testa se um numero e ounao zero, podemos usar as seguintes equacoes:

eZero 0 = TrueeZero (n+1) = False

Formas alternativas de definir esta ultima funcao, que usam a operacao de igualdadeentre termos (==) sao

• eZero x = if (x == 0) then True else False

• eZero x| (x == 0) = True| otherwise = False

• eZero x = (x == 0)

Exercıcios

2.1 Seja t = y:((x+4):[]). Calcule o resultado de:(1) t [x := x+y](2) t [y := y+1](3) (t [x := x+y]) [y := y+1](4) t [x := x+y, y := y+1]

2.2 Seja e a expressao x+ (x+ y). Mostre quais dos termos seguintes sao instanciasde e ou, alternativamente, contem sub-termos que sao instancias de e.(1) (2− (3 + 2)) + (2− (3 + 2))(2) 2− ((3 + 2) + ((3 + 2) + (5− 4)))(3) x+ (x+ x)(4) (x ∗ 2)− (y + (y + (x− y)))

2.3 Considere as seguintes definicoes:

Page 32: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

28 2. PROGRAMAS SIMPLES

quad x = x * x

circulo r = pi * (quad r)

Indique, para cada uma das expressoes abaixo indicadas, quais os redexesexistentes.(1) quad (circulo 3)(2) 3 + (circulo ((quad 3) * (quad 3)))(3) (circulo (quad 3)) + (quad 3)(4) circulo (circulo 3)

2.4 Para cada uma das expressoes da alınea anterior, apresente a expressao resultantede efectuar um passo de reducao usando uma estrategia eager (i.e., leftmost,innermost).

2.5 Repita a questao anterior usando uma estrategia lazy (i.e., outermost, leftmost).

2.6 (1) Escreva a definicao de uma funcao max2 que dado um par de numeros in-teiros, de como resultado o maior desses numeros.

(2) Qual o tipo desta funcao?(3) Escreva a versao curried desta funcao (suponha que esta nova funcao se

chama cmax2).(4) Mostre uma reducao possıvel para as seguintes expressoes

(a) max2 (max2 (3,5),(cmax2 2 4))(b) cmax2 (cmax2 3 (max2 (4,5)))

(5) Escreva a definicao de uma funcao max3 que dados 3 numeros, de comoresultado o maior dos tres.

(6) Reescreva esta funcao sem usar ifs (i.e., usando a funcao max2 definidaatras).

(7) Repita as duas alıneas anteriores para a funcao max4 que recebe comoparametros 4 numeros e retorna o maior deles.

(8) Quantas formas ha de reduzir a expressao max4 2 3 4 5?

2.7 Apresente definicoes das seguintes funcoes(1) iguais2 que, dados dois numeros de como resultado verdadeiro (True) se

esses numeros forem iguais e falso (False) caso eles sejam diferentes.(2) iguais4 que, dados 4 numeros de como resultado verdadeiro (True) se esses

numeros forem iguais e falso (False) caso pelo menos um seja diferente dosoutros.

(3) iguala20 que, dado um numero inteiro, retorna verdadeiro no caso dessenumero ser 20 e falso no outro caso:

(a) usando ifs(b) usando pattern matching

2.8 Num triangulo verifica-se sempre que o comprimento de qualquer lado e sempreinferior (ou igual) a soma dos comprimentos dos outros dois. Esta propriedadechama-se desigualdade triangular.

Page 33: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

2. PROGRAMAS SIMPLES 29

(1) Escreva a definicao de uma funcao tri que dados tres numeros, correspon-dentes aos comprimentos dos lados de um triangulo, de como resultadoverdadeiro se esses numeros obedecem a desigualdade triangular.

(2) Escreva uma definicao alternativa da funcao tri que tome partido do factode que basta testar se o maior dos numeros e menor (ou igual) a soma dosoutros dois.

2.9 Considere as seguintes definicoes:

fact :: Int -> Intfact n = product [1..n]

facT :: Integer -> IntegerfacT n = product [1..n]

(1) Qual o resultado de fact 3?(2) Qual o resultado de facT 6?(3) Qual o resultado de facT (fact 3)? Explique o porque deste resultado.(4) Repita as duas primeiras alıneas apos executar o comando :set +t. Diga

quais os resultados obtidos.(5) Qual o resultado de fact 20?(6) Qual o resultado de facT 20?

Page 34: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao
Page 35: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

CAPITULO 3

Funcoes recursivas

As funcoes que definimos atras tinham a seguinte caracterıstica: nos lados esquerdosdas equacoes que as definiam, apareciam apenas outras funcoes (mais simples), e nuncaa propria funcao que estava a ser definida.As funcoes que podem ser definidas desta forma nao sao suficientes para tornar alinguagem de programacao util. Esta medida de utilidade e normalmente conhecidapor poder expressivo da linguagem.Nos anos 30, dois cientistas definiram conceitos que tem sido adoptados como o ob-jectivo, em termos de poder expressivo, de qualquer linguagem de programacao:

• Alonzo Church definiu a notacao λ para a escrita de funcoes, e a ela ficouassociada a nocao de funcao λ-definıvel.• Alan Turing definiu o conceito de maquina de Turing e a ela ficou associada

a nocao de funcao computavel.

Mais tarde, o proprio Turing mostrou que estas duas nocoes sao equivalentes; istoe, que qualquer funcao computavel e λ-definıvel e que qualquer funcao λ-definıvel ecomputavel.Para que numa linguagem de programacao funcional se possam definir todas as funcoescomputaveis, e necessario poder definir funcoes recursivas. Dito de outra forma,talvez mais correcta, existem funcoes cuja definicao tem de ser recursiva.Uma definicao diz-se recursiva quando ela propria faz parte dessa definicao. Vejamosum exemplo – a funcao que calcula o factorial de um numero natural. Uma possıveldefinicao desta funcao e:

n! ={

1 Se n = 0n ∗ (n− 1)! Se n > 0

Ou, escrito na sintaxe Haskell

fact :: Int -> Intfact n| (n == 0) = 1| (n > 0) = n * (fact (n-1))

31

Page 36: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

32 3. FUNCOES RECURSIVAS

Esta definicao diz-se recursiva porque, em algumas das equacoes, o sımbolo a serdefinido (i.e., o sımbolo mais exterior do lado esquerdo – fact) aparece tambem nolado direito da equacao.Mas sera que esta definicao faz sentido? Isto e, sera que existe alguma funcao factque seja univocamente definida por estas equacoes? Neste curso vamos tentar daruma resposta a esta pergunta baseada numa visao operacional do calculo de expressoes(isto e, baseados no conceito de reducao que foi exposto atras).Vejamos entao qual o valor que e obtido ao calcular fact 4.

fact 4=⇒ 4 ∗ (fact 3)=⇒ 4 ∗ (3 ∗ (fact 2))=⇒ 4 ∗ (3 ∗ (2 ∗ (fact 1)))=⇒ 4 ∗ (3 ∗ (2 ∗ (1 ∗ (fact 0))))=⇒ 4 ∗ (3 ∗ (2 ∗ (1 ∗ 1)))=⇒ 4 ∗ (3 ∗ (2 ∗ 1))=⇒ 4 ∗ (3 ∗ 2)=⇒ 4 ∗ 6=⇒ 24

Esta reducao poe em evidencia o seguinte: ao reduzir uma expressao que contemfact, obtemos outra expressao onde fact tambem aparece; no entanto, esta segundainvocacao e feita com um argumento “mais simples”. Inevitavelmente, a simplicidadedeste argumento atinge o seu limite, i.e., passa a ser possıvel usar a equacao naorecursiva.Este tipo de recursividade e normalmente conhecido por recursividade primitiva1.

E de salientar a semelhanca entre esta forma de definir funcoes e a inducao natural.Nesta ultima, quando pretendemos provar que uma determinado predicado P e validopara todos os numeros naturais, i.e., quando queremos demonstrar a validade de

∀n∈Nat P (n)

aquilo que fazemos e

(1) mostrar que o predicado P e valido para 0;

1Dada uma definicao recursiva de uma funcao f (sobre os numeros naturais) dizemos queestamos perante recursividade primitiva se e so se as equacoes envolvidas nesta definicao sao:f 0 = c

f (n+1) = g n (f n)

Onde tanto a constante c e a funcao g sao nao recursivas ou entao sao definidas a custa de recursivi-dade primitiva.

• A constante c representa o valor da funcao para o caso “mais simples” do argumento.

• A funcao g representa a forma de construir o valor de f (n+1), a partir do valor de n e do

valor de f n.

Page 37: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

3. FUNCOES RECURSIVAS 33

(2) mostrar que se o predicado e valido para n, entao tambem o e para n+ 1.

Este segundo passo e normalmente conhecido por passo indutivo e corresponde asegunda equacao da definicao de factorial – tambem esta nos permite obter o valor defact (n+1) a partir do valor de fact n.Vejamos agora um outro exemplo – o calculo da potencia de um numero. Pretende-seentao escrever a definicao de uma funcao pot tal que

pot n x = xn

Para isso podemos dividir o problema em:

• qual o valor desta funcao para 0?• como calcular o valor de xn a partir de x e de xn−1?

Da resposta a estas duas perguntas resulta a seguinte definicao.

pot x n| (n == 0) = 1| (n > 0) = x * (pot x (n-1))

Como ja referimos atras, uma forma alternativa para escrever esta mesma definicaoconsiste no uso de varias equacoes – uma para cada um dos padroes possıveis dosargumentos.

pot x 0 = 1pot x (n+1) = x * (pot x n)

Alternativamente, poderıamos incorporar a concordancia de padroes no lado direito daequacao. Isto faz-se recorrendo a construcao case ... of .... Neste caso terıamos.

pot x n =case n of0 -> 1m+1 -> x * (pot x m)

Uma outra definicao da funcao que calcula a potencia de um numero e:

potencia x n =if (n == 0)then 1else x * (potencia x (n-1))

Convem no entanto notar que estas duas definicoes nao sao equivalentes. Vejamosporque:

*FRecursivas> pot 2 10

1024

Page 38: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

34 3. FUNCOES RECURSIVAS

*FRecursivas> potencia 2 10

1024

*FRecursivas> pot 2 (-1)

*** Exception: FRecursivas.hs:6: Non-exhaustive patterns in function pot

*FRecursivas> potencia 2 (-1)

*** Exception: stack overflow

Na primeira definicao (pot) a funcao nao se encontra definida para valores negativosdo expoente. O erro que ocorre quando tentamos calcular pot 2 (-1) esta por issomesmo relacionado com o facto de que este termo nao concorda com nenhum dospadroes possıveis.Na segunda definicao (potencia) o calculo para valores negativos do expoente naotermina.Vamos no entanto apresentar aqui mais um exemplo para consolidar estas ideias – ocalculo do n-esimo numero de Fibonnacci.A definicao matematica deste numero e

Fib (n) =

1 Se n = 01 Se n = 1Fib (n− 1) + Fib (n− 2) Se n > 1

Esta definicao pode ser traduzida directamente para a sintaxe Haskell

fib n| (n == 0) = 1| (n == 1) = 1| (n > 1) = (fib (n-1)) + (fib (n-2))

Usando pattern-matching, esta mesma definicao pode ser escrita como.

fib 0 = 1fib 1 = 1fib (n+2) = (fib n) + (fib (n+1))

Uma outra possibilidade para escrever a definicao da funcao seria.

fib n| (n < 2) = 1| (n >= 2) = (fib (n-1)) + (fib (n-2))

Note-se que esta ultima definicao nao e equivalente as duas primeiras – com estaultima, pode-se concluir que fib (-1) = 1 enquanto que tal nao acontece com asprimeiras.

Page 39: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

3. FUNCOES RECURSIVAS 35

Comparemos as definicoes das funcoes fib e fact (cf. pag. 31). Enquanto que na parterecursiva da funcao fact se faz uma unica referencia a propria funcao, na definicao defib fazem-se duas. Vejamos qual a consequencia desta diferenca. Para isso vamos usaro hugs, instruindo-o para, no final de cada calculo, apresentar o numero de reducoesefectuadas (usando o comando :set +s).

Main> :set +s

Main> fact 5

120

(88 reductions, 181 cells)

Main> fact 10

3628800

(163 reductions, 353 cells)

Main> fact 15

1307674368000

(238 reductions, 534 cells)

Main> fact 20

2432902008176640000

(313 reductions, 722 cells)

Main> fact 25

15511210043330985984000000

(388 reductions, 919 cells)

Main> fact 30

265252859812191058636308480000000

(463 reductions, 1126 cells)

Main>

Podemos ver que o numero de reducoes efectuadas, aumenta com o valor do argumento.Este aumento e, no entanto, linear – quando o valor do argumento duplica, o mesmoacontece com o numero de reducoes.Vejamos agora o que se passa com a funcao fib.

Main> fib 3

3

(65 reductions, 102 cells)

Main> fib 6

13

(313 reductions, 513 cells)

Main> fib 9

55

(1357 reductions, 2239 cells)

Main> fib 12

233

(5781 reductions, 9554 cells)

Main> fib 15

987

(24521 reductions, 40536 cells)

Main> fib 18

Page 40: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

36 3. FUNCOES RECURSIVAS

4181

(103905 reductions, 171779 cells, 1 garbage collection)

Main>

Aqui o aumento e exponencial – ao mutiplicarmos o valor do argumento por 2, onumero de reducoes aumenta 4 vezes; aumentando o valor do argumento 6 vezes, onumero de reducoes aumenta 36 vezes! Veremos mais a frente, outros exemplos defuncoes cujo esforco de calculo se situa entre estes dois casos.Mas para ja, vamos tentar obter uma definicao da funcao fib (para a distinguir dadefinicao anterior, chamemos-lhe fibbonnacci) cuja performance seja melhor do quea da apresentada acima.Vamos comecar por definir uma funcao fib2 que, dado um numero n maior do que 1,da como resultado um par de numeros (a,b) tal que

• a e o n-esimo numero de Fibonnacci (isto e, a = fib n), e• b = fib (n-1)

A definicao desta funcao passa pela resposta as seguintes questoes:

• Qual o valor de fib2 1?• Sabendo o valor de fib2 n (isto e, o par (fib n, fib (n-1))), qual o valor

de fib2 (n+1)? Note-se que, pelo que foi dito acima,fib2 (n+1) = (fib (n+1), fib ((n+1)-1)

= (fib (n+1), fib n)

Da resposta a estas duas questoes resulta imediatamente a seguinte definicao.

fib2 1 = (1,1)fib2 (n+1) =let (a,b) = fib2 nin (a+b,a)

Com esta funcao disponıvel, e muito facil definir a funcao de calculo do n-esimo numerode Fibonnacci.

fibonnacci 0 = 1fibonnacci (n+1) = fst (fib2 (n+1))

Vejamos agora o comportamento desta funcao.Main> fibonnacci 5

8

(65 reductions, 125 cells)

Main> fibonnacci 10

89

(110 reductions, 226 cells)

Main> fibonnacci 15

Page 41: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

3. FUNCOES RECURSIVAS 37

987

(155 reductions, 327 cells)

Main> fibonnacci 20

10946

(200 reductions, 430 cells)

Main> fibonnacci 25

121393

(245 reductions, 535 cells)

Main> fibonnacci 30

1346269

(290 reductions, 641 cells)

Main>

Como podemos ver, esta definicao e tambem linear – a medida que o valor do argu-mento aumenta linearmente, o numero de reducoes tambem aumenta linearmente.Uma tecnica muitas vezes usada para obter programas mais eficientes e o uso deacumuladores. Para entendermos como esta tecnica e usada vamos fazer uma analiseoperacional de algumas definicoes apresentadas atras.Relembremos a definicao da funcao fact.

fact :: Int -> Intfact 0 = 1fact (n+1) = (n+1) * (fact n)

De um ponto de vista operacional, a segunda equacao da definicao acima, pode ler-secomo “para calcular o factorial de n+1, calcula-se o factorial de n e multiplica-se esteresultado por n+1”. Esta visao operacional e talvez mais evidente quando tentamossimular a reducao de uma expressao contendo fact.

fact 4=⇒ 4 ∗ (fact 3)=⇒ 4 ∗ (3 ∗ (fact 2))=⇒ 4 ∗ (3 ∗ (2 ∗ (fact 1)))=⇒ 4 ∗ (3 ∗ (2 ∗ (1 ∗ (fact 0))))=⇒ 4 ∗ (3 ∗ (2 ∗ (1 ∗ 1)))=⇒ 4 ∗ (3 ∗ (2 ∗ 1))=⇒ 4 ∗ (3 ∗ 2)=⇒ 4 ∗ 6=⇒ 24

Nesta reducao podemos ver duas fases distintas:

• numa primeira fase, vamos desfazendo a definicao de fact, obtendo assimuma expressao cada vez mais longa;

Page 42: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

38 3. FUNCOES RECURSIVAS

• na segunda fase, e depois de ja nao aparecer qualquer ocorrencia de fact,vamos construindo o resultado a partir da reducao das tarefas que tinhamsido deixadas suspensas na fase anterior.

Uma outra estrategia para resolver este mesmo problema passa por usar uma formade ir memorizando resultados parciais. Vejamos entao como tal estrategia pode serseguida.

factorial :: Int -> Intfactorial n = factac 1 n

factac :: Int -> Int -> Intfactac ac 0 = acfactac ac (n+1) = factac (ac * (n+1)) n

Ao tentarmos reduzir factorial 4 obtemos agora.

factorial 4=⇒ factac 1 4=⇒ factac (1 ∗ 4) 3=⇒ factac ((1 ∗ 4) ∗ 3) 2=⇒ factac (((1 ∗ 4) ∗ 3) ∗ 2) 1=⇒ factac ((((1 ∗ 4) ∗ 3) ∗ 2) ∗ 1) 0=⇒ (((1 ∗ 4) ∗ 3) ∗ 2) ∗ 1=⇒ ((4 ∗ 3) ∗ 2) ∗ 1=⇒ (12 ∗ 2) ∗ 1=⇒ 24 ∗ 1=⇒ 24

Note-se que os calculos que eram suspensos na definicao original se encontram agoraacumulados no primeiro parametro da funcao factac. Dizemos por isso que esteprimeiro parametro tem o papel de acumulador.Em alguma altura do calculo (neste caso quando o segundo parametro e zero), tudo oque temos a fazer e devolver (i.e., calcular) o valor do acumulador.Esta tecnica pode ser aplicada a muitos outros problemas que surgirao ao longo destetexto.

Page 43: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

3. FUNCOES RECURSIVAS 39

Vejamos um outro exemplo – a funcao fib que calcula o n-esimo numero de Fib-bonacci. O calculo de fib 4 pode ser feito da seguinte forma.

fib 4=⇒ (fib 3) + (fib 2)=⇒ ((fib 2) + (fib 1)) + (fib 2)=⇒ (((fib 1) + (fib 0)) + (fib 1)) + (fib 2)=⇒ (((fib 1) + (fib 0)) + (fib 1)) + ((fib 1) + (fib 0))=⇒ ...=⇒ ((1 + 1) + 1) + (1 + 1)=⇒ ...=⇒ 5

Como podemos ver por estas reducoes, o calculo de fib 4 envolve o calculo duplicadode fib 2. Vimos atras uma estrategia que nos permitiu melhorar substancialmente aperformance desta definicao – em vez de calcular apenas fib n, calculavamos tambemfib (n-1). Seguindo esta mesma linha de raciocınio, podemos definir uma funcaopara o calculo do n-esimo numero de Fibonnacci usando acumuladores.

fibon :: Integer -> Integerfibon n = fibon’ 1 1 n

fibon’ :: Integer -> Integer -> Integer -> Integerfibon’ ac1 ac2 0 = ac2fibon’ ac1 ac2 1 = ac2fibon’ ac1 ac2 (n+2) = fibon’ ac2 (ac1 + ac2) (n+1)

Exercıcios

3.1 Escreva a definicao de uma funcao que calcule o maior divisor de um numero(excluindo o proprio numero!).

3.2 Escreva a definicao de uma funcao que verifique se um dado numero e primo.

3.3 Euclides definiu um processo para calculo do maior divisor comum entre doisnumeros (mdc):• o maior divisor comum entre dois numeros iguais e esse proprio numero;• o maior divisor comum entre dois numeros diferentes e igual ao maior divisor

comum entre o menor deles e a diferenca entre eles.Use esta definicao para definir a funcao mdc que calcula o maximo divisor comumentre dois numeros. Qual o resultado da funcao que definiu quando um dos seusargumentos e zero?

3.4 Escreva a definicao de uma funcao qdig que calcule quantos dıgitos sao necessariospara escrever um numero; por exemplo, qdig 14 = 2, qdig 1221 = 4

Page 44: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

40 3. FUNCOES RECURSIVAS

3.5 Escreva a definicao de uma funcao limraiz que testa se a raiz quadrada de umnumero esta dentro de um dado intervalo; por exemplo limraiz 4 (1,3) = Trueenquanto que limraiz 4 (3,5) = False.

3.6 Usando a funcao definida na alınea anterior, escreva a definicao de uma funcaoque calcule a raiz quadrada de um numero com 3 dıgitos significativos (Sugestao:lembre-se que para todo o numero positivo, 0 ≤

√x ≤ x).

3.7 O quadrado de um numero n pode ser calculado como a soma dos n numerosımpares. Usando esta informacao, defina a funcao que calcula o quadrado de umnumero.

Page 45: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

CAPITULO 4

Listas

Vimos atras que uma forma de estruturar tipos noutros mais complexos era pelo usodo produto cartesiano. Assim, uma expressao composta por 3 inteiros seria do tipo(Int,Int,Int) enquanto que uma expressao composta por 4 inteiros seria do tipo(Int,Int,Int,Int). Estes dois tipos sao disjuntos.E claro que o tipo de uma dada expressao nao muda e que por isso poderia parecerabsurdo querer definir entidades que por vezes sao compostas por 3 inteiros e outrasvezes por 4. Mas vejamos o seguinte exemplo: queremos definir uma funcao que,dado um numero natural, de como resultado os seus divisores. Qual seria entao otipo do resultado desta funcao? A partida, nao sabemos quantos divisores um dadonumero tem, e por isso nao podemos usar o produto cartesiano para descrever estetipo. Precisamos de algo mais flexıvel. E para isso e que vamos usar listas.Dado um tipo a, o tipo [a] e o tipo das listas (ou sequencias) cujos elementos sao dotipo a. Note-se que, ao contrario do que acontecia com o produto cartesiano, todos oselementos de uma lista tem de ser do mesmo tipo.A lista mais simples que existe e aquela que nao tem qualquer elemento. Em Haskell,esta lista e representada por [].Dada uma lista xx de elementos do tipo a e um elemento x tambem do tipo a, podemosconstruir uma nova lista (do tipo [a]) em que o primeiro elemento e x e os seguintessao os elementos de xx. Esta lista em Haskell e denotada por x:xx.A lista das 5 vogais e por isso representada por

’a’:(’e’:(’i’:(’o’:(’u’:[]))))

e tem como tipo [Char]. Algumas formas alternativas para definir esta lista sao

• [’a’,’e’,’i’,’o’,’u’]• ’a’:(’e’:[’i’,’o’,’u’])

No caso particular em que o tipo das componentes da lista sao caracteres, e costumedizer que estamos perante uma string. Existe, pre-definida, uma abreviatura paraeste tipo nas versoes standard do Haskell.

type String = [Char]

41

Page 46: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

42 4. LISTAS

Apesar de serem um caso particular de listas, existe ainda uma sintaxe alternativapara representar uma string – limitando os varios caracteres pelo caracter ‘"’. Assimsendo, uma outra representacao alternativa da lista apresentada acima, e "aeiou".Convem fazer aqui um pequeno parentesis para reforcar a diferenca entre as expressoesa, ’a’ e "a":

• a e um nome de uma expressao (uma variavel);• ’a’ e um caracter;• "a" e uma lista de caracteres (que so tem um elemento – o caracter ’a’).

O mesmo se pode dizer das expressoes 1, ’1’ e "1":

• 1 e um numero;• ’1’ e um caracter;• "1" e uma lista de caracteres (que so tem um elemento – o caracter’1’).

Existem pre-definidas muitas funcoes sobre listas. Algumas das mais simples e maisutilizadas sao:

• length – que calcula o comprimento de uma lista;• null – que testa se uma lista e vazia;• head – que calcula o primeiro elemento de uma lista; esta funcao e parcial,

i.e., nao esta definida para todas as listas (a excepcao e a lista vazia);• tail – que calcula a cauda da lista (i.e., a lista sem o primeiro elemento);• last – que calcula o ultimo elemento da lista;• init – que calcula a lista sem o ultimo elemento;• reverse – que inverte a ordem ds elementos de uma lista;• (++) – que calcula a concatenacao (i.e., a justaposicao) de duas listas.

Main> length [10,2,30]

3

Main> length "Haskell"

7

Main> null "Haskell"

False

Main> null ""

True

Main> null []

True

Main> head [23, 21, 30]

23

Main> head "Haskell"

’H’

Main> head []

Program error: {head []}

Main> tail [23, 21, 30]

[21, 30]

Page 47: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

4. LISTAS 43

Main> tail "Haskell"

"askell"

Main> tail []

Program error: {tail []}

Main> last [23, 21, 30]

30

Main> last "Haskell"

’l’

Main> init [23, 21, 30]

[23, 21]

Main> init "Haskell"

"Haskel"

Main> reverse [23, 21, 30]

[30, 21, 23]

Main> reverse "Haskell"

"lleksaH"

Main> [23, 21, 30] ++ [1, 1, 1]

[23, 21, 30, 1, 1, 1]

Main> "Haskell" ++ "Curry"

"HaskellCurry"

As listas que vimos ate aqui foram todas definidas atraves da enumeracao dos seuselementos. Uma outra forma de definir listas e por compreensao. Esta forma emuito semelhante a que usamos frequentemente para definir conjuntos. Por exemplo,para definir o conjunto dos numeros ımpares, dirıamos

{2 ∗ n+ 1 | n ∈ N}

Em Haskell, para definir a lista dos numeros ımpares de 1 a 9, podemos escrever

[2*n - 1 | n <- [1,2,3,4,5] ]

Alternativamente, esta mesma lista pode ser definida como

[n | n <- [1..9] , odd n]

Esta ultima definicao pode ser lida como: a lista de todos os numeros n tais que n

vem da lista [1..9] e para os quais a funcao odd da como resultado verdadeiro.Estas duas definicoes sao equivalentes pois a funcao odd :: Int -> Bool testa seum numero e ımpar.A lista [1..9] nada mais e do que a lista de todos os numeros inteiros desde 1 ate 9.Esta notacao e inspirada na habitual notacao de intervalos e pode ainda tomar outrasformas. Por exemplo, a lista [1,4..10] representa a lista dos numeros inteiros emque:

• o primeiro elemento e 1;• todos os elementos sao menores ou iguais a 10;• a diferenca entre um elemento e o anterior e 3 (i.e., 4-1).

Page 48: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

44 4. LISTAS

Ou seja, representa a lista [1,4,7,10]. Daı que, a lista dos numeros ımpares de 1 a9 possa ainda ser definida como

[1,3..9]

Mas voltemos entao as definicoes por compreensao. Tal como na estrutura let...in...,as definicoes por compreensao sao construıdas a partir de blocos onde se introduzemnomes locais – no caso acima o bloco n <- [1..9] introduz o nome n. As definicoespor compreensao tambem podem ser compostas por varios destes blocos. No entanto,nas definicoes por compreensao a ordem pela qual estes blocos aparecem e relevante.Vejamos um exemplo ilustrativo desta situacao.O produto cartesiano dos conjuntos A e B pode ser definido como

A×B = {(a, b) | a ∈ A ∧ b ∈ B}Algo semelhante se pode escrever em Haskell:

[(a,b) | a <- [1..5] , b <- [10..13] ]

Ao calcularmos o valor desta expressao, o resultado obtido e:Main> [(a,b) | a <- [1..5] , b <- [10..13]]

[(1,10), (1,11), (1,12), (1,13),

(2,10), (2,11), (2,12), (2,13),

(3,10), (3,11), (3,12), (3,13),

(4,10), (4,11), (4,12), (4,13),

(5,10), (5,11), (5,12), (5,13)]

No entanto, ao trocarmos a ordem dos dois blocos, obtemosMain> [(a,b) | b <- [10..13] ,a <- [1..5] ]

[(1,10), (2,10), (3,10), (4,10), (5,10),

(1,11), (2,11), (3,11), (4,11), (5,11),

(1,12), (2,12), (3,12), (4,12), (5,12),

(1,13), (2,13), (3,13), (4,13), (5,13)]

Este exemplo e elucidativo do “funcionamento” das definicoes por compreensao:• no primeiro caso, aparecem primeiro todos os pares cuja primeira compo-

nente e 1, seguidos de todos os pares em que a primeira componente e 2, eassim sucessivamente, ate que por ultimo aparecem todos os elementos emque a primeira componente e 5;• no segundo caso, aparecem primeiro todos os pares em que a segunda com-

ponente e 10, seguidos de todos os pares em que a segunda componente e 11,e assim sucessivamente, ate que por ultimo aparecem todos os elementos emque a segunda componente e 5.

Ve-se neste exemplo uma dependencia clara entre os varios blocos. De uma formageral, ao fazermos as declaracoes

[... | a1 <- l1 , a2 <- l2, ...]

por cada valor que a1 tome, o valor de a2 vai tomar todos os valores possıveis (isto e,todos os elementos de l2).

Page 49: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

4. LISTAS 45

Vimos tambem que nestas definicoes podem aparecer predicados (i.e., funcoes queretornam verdadeiro ou falso). Estas condicoes funcionam como filtros. No seguinteexemplo

Main> [(a,b) | a <- [1..5] , odd a, b <- [10..13]]

[(1,10), (1,11), (1,12), (1,13), (3,10), (3,11), (3,12),

(3,13), (5,10), (5,11), (5,12), (5,13)]

a variavel a nao percorre todos os elementos da lista [1..5]; mas apenas seleccionadosaqueles para os quais a funcao odd da como resultado verdadeiro (i.e., os ımpares).Note-se que o mesmo resultado final e obtido trocando a ordem pela qual o filtro eaplicado.

Main> [(a,b) | a <- [1..5] , b <- [10..13], odd a]

[(1,10), (1,11), (1,12), (1,13), (3,10), (3,11), (3,12),

(3,13), (5,10), (5,11), (5,12), (5,13)]

Estas duas expressoes tem realmente o mesmo valor. No entanto o calculo da segundae menos eficiente. Usemos o hugs para comprova-lo.

Prelude> :set +s

Prelude> [(a,b) | a <- [1..5] , odd a, b <- [10..13]]

[(1,10),(1,11),(1,12),(1,13),(3,10),(3,11),(3,12),

(3,13),(5,10),(5,11),(5,12),(5,13)]

(669 reductions, 1266 cells)

Prelude> [(a,b) | a <- [1..5] , b <- [10..13], odd a]

[(1,10),(1,11),(1,12),(1,13),(3,10),(3,11),(3,12),

(3,13),(5,10),(5,11),(5,12),(5,13)]

(910 reductions, 1486 cells)

Prelude>

Vejamos agora alguns exemplos do uso de listas por compreensao na definicao defuncoes.

• calculo dos divisores de um numero

divisores n = [p | p <- [1..n] , (n ‘mod‘ p) == 0 ]

Note-se que usamos o processo de conversao de operadores prefixos em infixosreferido no Capıtulo 1. Sem usar este mecanismo, a definicao anterior seria

divisores n = [p | p <- [1..n] , (mod n p) == 0 ]

Podemos ver o resultado de executar esta funcao.Main> divisores 36

[1,2,3,6,9,12,18,36]

Main> divisores 28

[1,2,4,7,14,28]

Main> divisores 13

[1,13]

Main>

Page 50: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

46 4. LISTAS

Com esta funcao podemos definir uma funcao para verificar se um numero eou nao primo

primo x = (divisores x) == [1,x]

• Calculo dos divisores comuns de um numero:

divcomuns n m = [p | p <- divisores n , (m ‘mod‘ p) == 0]

Podemos agora calcular o maximo divisor comum de dois numeros; paraisso vamos usar a funcao maximum :: [a] -> a que calcula o maior doselementos de uma lista nao vazia.

mdc n m = maximum (divcomuns n m)

A tecnica apresentada no capıtulo anterior para definir funcoes numericas usandorecursividade pode ser justificada pela definicao do conjunto dos numeros naturais.Este conjunto e definido como o menor conjunto que satisfaz as seguintes propriedades:

• 0 e um numero natural• se n e um numero natural entao n+ 1 tambem o e.

Desta definicao, pode-se concluir que qualquer que seja o numero natural n, ou n ezero, ou entao existe um outro numero natural n′ tal que n e o sucessor de n′ (i.e.,n = n′ + 1).Definicoes como estas dizem-se indutivas e ha muitos conjuntos que se podem definirindutivamente.O numero 0 e a operacao sucessor chamam-se os construtores do conjunto dosnumeros naturais. Note-se ainda que, ao dizermos que o conjunto dos numeros natu-rais e o menor dos conjuntos que satisfazem essas propriedades, fica automaticamentedemonstrado que qualquer numero natural pode ser obtido a partir de um numerofinito de aplicacoes do construtor sucessor a 0.

Um outro conjunto que pode ser definido indutivamente e o das listas. Aqui a definicaoe ligeiramente mais complicada, pois queremos definir o conjunto das listas cujoselementos sao de um determinado conjunto. De qualquer forma a definicao e bastantesemelhante a da dos numeros naturais:Dado um conjunto X, o conjunto das listas cujos elementos sao elementos de X define-se como o menor conjunto que satisfaz as seguintes propriedades:

• a lista vazia ([]) e uma lista de elementos de X;• se xx e uma lista de elementos de X e x e um elemento de X, entao x:xx

tambem e uma lista de elementos de X.

Page 51: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

4. LISTAS 47

Os construtores deste conjuto sao: a lista vazia – [] – e a operacao “:”. Tambem aquipodemos concluir que qualquer lista pode ser obtida a partir de um numero finito deaplicacoes do construtor “:” a lista vazia.Alem disso, e por analogia com o que acontece no conjunto dos numeros naturais,podemos a partir desta definicao, extrair um metodo de construcao de funcoes sobrelistas – enumerando o resultado das funcoes para cada uma das possibilidades deconstrucao das listas.Vejamos um exemplo: a definicao da funcao que dada uma lista, da como resultadoo seu comprimento (isto e, a funcao length referida atras). Para definir esta funcaoprecisamos de responder as seguintes questoes:

• Qual o comprimento da lista vazia?• Como obter o comprimento da lista x:xx a partir do valor x e do valor do

comprimento da lista xx?

Da resposta a estas duas questoes resulta a seguinte definicao recursiva.

comprimento [] = 0comprimento (x:l) = 1 + (comprimento l)

O nosso proximo exemplo e a definicao da funcao de concatenacao de listas (a operacao++ ja referida). A forma mais eficiente de definir esta funcao consiste em responder asperguntas:

• Qual o resultado de concatenar uma lista l a lista vazia?• Como obter a concatenacao da lista x:xx com uma lista l a partir do valor

de x e da concatenacao de xx com a lista l?

A resposta a estas duas questoes tem como resultado a seguinte definicao recursiva:

concatena [] l = lconcatena (x:xs) l = x:(concatena xs l)

Vejamos mais um exemplo: a funcao que inverte uma lista (isto e, a funcao reverse).Continuando na mesma linha de raciocınio, para definir esta funcao devemos tentarencontrar resposta para as questoes:

• Qual o resultado de inverter a lista vazia?• Como obter o resultado de inverter a lista x:xx a partir do conhecimento dex e do resultado de inverter a lista xx?

A resposta a primeira questao e muito simples – ao invertermos a lista vazia devemosobter uma lista vazia.Quanto a segunda questao, vejamos um exemplo. Suponhamos que queremos in-verter a lista [1,2,3,4] e ja conhecemos o resultado de inverter a lista [2,3,4](inverte[2,3,4] = [4,3,2]). Como obter entao o resultado pretendido ([4,3,2,1])a partir de 1 e de [4,3,2]?

Page 52: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

48 4. LISTAS

Podemos por isso apresentar a seguinte definicao.

inverte [] = []inverte (x:xx) = (inverte xx) ++ [x]

Vejamos entao um exemplo de como esta definicao “funciona”.

inverte [1,2,3]=⇒ (inverte [2,3]) ++ [1]=⇒ ((inverte [3]) ++ [2]) ++ [1]=⇒ (((inverte []) ++ [3]) ++ [2]) ++ [1]=⇒ (([] ++ [3]) ++ [2]) ++ [1]=⇒ ([3] ++ [2]) ++ [1]=⇒ . . .=⇒ [3,2] ++ [1]=⇒ . . .=⇒ [3,2,1]

Vejamos agora um exemplo ligeiramente mais complicado: a definicao da funcao zip(pre-definida em Haskell) que, dadas duas listas retorna uma lista de pares. Cadaelemento desta lista e composto pelos elementos dessa posicao em cada uma das listas.Por exemplo,

Main> zip [1,2,3] [’a’,’b’,’c’]

[(1,’a’),(2,’b’),(3,’c’)]

Vamos em primeiro lugar escrever uma definicao que (de alguma forma) assume queambas as listas tem o mesmo numero de elementos.

zip [] [] = []zip (x:xs) (y:ys) = (x,y):(zip xs ys)

A primeira destas equacoes da-nos o valor da funcao quando ambas as listas sao vazias.A segunda equacao, mostra como construir o valor de zip (x:xs) (y:ys) a partirdos valores de x, y e de (zip xs ys).Vejamos alguns exemplos de calculo usando esta definicao:

• zip [1,2,3] [’a’,’b’,’c’]=⇒ (1,’a’):(zip [2,3] [’b’,’c’])=⇒ (1,’a’):(2,’b’):(zip [3] [’c’])=⇒ (1,’a’):(2,’b’):(3,’c’):(zip [] [])=⇒ (1,’a’):(2,’b’):(3,’c’):[]

• zip [1] [’a’,’b’,’c’]=⇒ (1,’a’):(zip [] [’b’,’c’])

• zip [1,2,3] [’a’]=⇒ (1,’a’):(zip [2,3] [])

Page 53: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

4. LISTAS 49

Como podemos ver, esta definicao nao funciona nos casos em que as duas listas nao temo mesmo numero de elementos. Para tratar estes casos podemos escrever a seguintedefinicao.

zip [] [] = []zip [] (y:ys) = []zip (x:xs) [] = []zip (x:xs) (y:ys) = (x,y):(zip xs ys)

As definicoes que apresentamos ate aqui e que usavam concordancia de padroes, tin-ham a seguinte caracterıstica: os padroes usados eram exclusivos. Isto tem comoconsequencia que a ordem pela qual os padroes aparecem nao e relevante – apenasum deles pode ser usado. Esta caracterıstica e obrigatoria em muitas linguagens deprogramacao funcional. Nao existindo este requisito, tem de se estabelecer uma ordempela qual devem ser aplicados padroes nao exclusivos. Em Haskell, essa ordem e aordem pela qual os padroes aparecem no texto do programa. Assim sendo, quando sepuderem aplicar dois padroes, escolhe-se o primeiro.Tendo isto em consideracao, a definicao em Haskell da funcao zip pode ser resumidacomo se segue.

zip (x:xs) (y:ys) = (x,y):(zip xs ys)zip a b = []

Vejamos agora alguns exemplos de uso de acumuladores no processamento de listas.A funcao que calcula o comprimento de uma lista pode ser definida a custa de

acumuladores.

comprimento l = compAC 0 l

compAC ac [] = accompAC ac (x:xs)= compAC (ac + 1) xs

Esta definicao usa uma funcao auxiliar – compAC – cujo primeiro argumento fun-ciona como um acumulador dos resultados intermedios. O calculo de comprimento

Page 54: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

50 4. LISTAS

"Haskell" pode ser feito da seguinte forma.

comprimento "Haskell"=⇒ compAC 0 "Haskell"=⇒ compAC 1 "askell"=⇒ compAC 2 "skell"=⇒ compAC 3 "kell"=⇒ compAC 4 "ell"=⇒ compAC 5 "ll"=⇒ compAC 6 "l"=⇒ compAC 7 ""=⇒ 7

Vejamos um ultimo exemplo: a definicao da funcao inverte para efectuar a inversao deuma lista (correspondente a funcao reverse pre-definida em Haskell). Vimos atrasuma definicao dessa funcao. Aı era usada a operacao de concatenacao de sequencias(++). Ora a definicao dessa operacao mostra que temos que percorrer toda a primeirasequencia, o que torna a definicao apresentada muito pouco eficiente.Vejamos agora uma definicao que usa acumuladores.

invAc l = invAC [] l whereinvAC ac [] = acinvAC ac (x:xs) = invAC (x:ac) xs

O calculo de invAc "Haskell" pode ser feito da seguinte forma.

invAc "Haskell"=⇒ invAC "" "Haskell"=⇒ invAC "H" "askell"=⇒ invAC "aH" "skell"=⇒ invAC "saH" "kell"=⇒ invAC "ksaH" "ell"=⇒ invAC "eksaH" "ll"=⇒ invAC "leksaH" "l"=⇒ invAC "lleksaH" ""=⇒ "lleksaH"

Comparemos os numeros de reducoes necessarios para efectuar calculos com estafuncao e com a funcao definida na pagina 48.

Main> inverte "Haskell Curry"

"yrruC lleksaH"

(263 reductions, 519 cells)

Main> invAc "Haskell Curry"

"yrruC lleksaH"

(169 reductions, 273 cells)

Main> length (inverte [1..10])

Page 55: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

4. LISTAS 51

10

(245 reductions, 413 cells)

Main> length (inverte [1..100])

100

(6770 reductions, 17379 cells)

Main> length (inverte [1..1000])

1000

(517520 reductions, 1523530 cells, 16 garbage collections)

Main> length (invAc [1..10])

10

(191 reductions, 269 cells)

Main> length (invAc [1..100])

100

(1721 reductions, 2430 cells)

Main> length (invAc [1..1000])

1000

(17021 reductions, 24031 cells)

Como podemos ver, em ambos os casos o numero de reducoes aumenta com o compri-mento da sequencia a inverter. Esse aumento e linear no caso de invAc e quadraticono caso de inverte.

Exercıcios

4.1 Para qualquer numero natural n, o unico divisor de n maior do que n/2 e oproprio n. Modifique a definicao da funcao divisores de forma a ter este facto emconta. (Sugestao: nao e necessario testar se os numeros que vao de n/2 a n saodivisores de n – destes apenas n o e!).

4.2 Usando a funcao sum que soma todos os elementos de uma lista, defina umafuncao perfeito que testa se um numero e ou nao perfeito (um numero n diz-seperfeito se a soma de todos os seus divisores (diferentes de n) e igual a n).

4.3 Defina uma funcao que conte quantos numeros pares existem numa lista.

4.4 Usando definicoes por compreensao, defina uma funcao pot tal que pot x n =xn

4.5 Quais os resultados de: [2..2], [2,7..4] e [0.0,0.3..1.0]?

4.6 Quais os resultados de sum[1..5] e product [1.0,2.5..5.0]?

4.7 Defina a funcao maxList:: [Int] -> Int que para uma dada lista de inteirosda como resultado o maior deles.

4.8 Defina a funcao sumList:: [Int] -> Int que para uma dada lista de inteirosda como resultado a soma dos seus elementos.

4.9 Defina a funcao sqrList:: [Int] -> [Int] que para uma dada lista de in-teiros da como resultado a lista dos quadrados dos seus elementos.

Page 56: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

52 4. LISTAS

4.10 Defina a funcao numList:: Int -> [Int] -> Int que da como resultado onumero de ocorrencias de um dado numero inteiro numa lista de inteiros.

4.11 Defina a funcao memList:: Int -> [Int] -> Bool que verifica se um dadonumero inteiro pertence ou nao a uma dada lista de inteiros.

4.12 Defina a funcao conList :: [[t]] -> [t] que da como resultado a con-catenacao das listas passadas como argumento.

4.13 Considere as seguintes definicoes:

type Aluno = (NumAluno,NomAluno,TelAluno)type NumAluno = Inttype NomAluno = Stringtype TelAluno = String

numAluno(x,y,z) = xnomAluno(x,y,z) = ytelAluno(x,y,z) = z

(1) Defina a funcao telefones :: [Aluno] -> [TelAluno] que da comoresultado a lista de telefones dos alunos.

(2) Defina a funcaovelhosAlunos :: [Aluno] -> [(NumAluno,NomAluno)]

que da como resultado os numeros e os nomes dos alunos cujo numero dealuno seja menor ou igual a 5000.

4.14 Dada uma lista de horas de partida e de chegada de uma viagem, pretende-secalcular:• o tempo total de viagem efectiva;• o tempo total de espera.

Por exemplo, se a dita lista for

[(9.5, 10.25), (11.3, 12.75) , (13.5, 14.75)]

significa que a viagem teve tres etapas:• a primeira comecou as 9.5 e terminou as 10.25;• a segunda comecou as 11.3 e terminou as 12.75;• a terceira comecou as 13.5 e terminou as 14.75;

Por isso• o tempo total de viagem efectiva e dado por (10.25 - 9.5) + (12.75 -11.3) + (14.75 - 13.5);• o tempo total de espera e dado por (11.3 - 10.25) + (13.5 - 12.75).

(1) Defina o tipo Viagem (da qual a lista acima e um habitante).(2) Escreva uma funcao que testa se uma viagem esta bem construıda (i.e., se

para cada etapa, o tempo de chegada e superior ao de partida, e que a etapaseguinte comeca depois de a etapa anterior ter terminado).

Page 57: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

4. LISTAS 53

(3) Escreva uma funcao que, dada uma viagem valida, calcule o tempo total deviagem efectiva.

(4) Escreva uma funcao que calcule o tempo total de espera.(5) Escreva uma funcao que calcule o tempo total da viagem (a soma dos tempos

de espera e de viagem efectiva).

4.15 Defina a funcao merge :: [a] -> [a] -> [a] que dadas duas listas orde-nadas da como resultado a fusao dessas listas numa unica lista ordenada. Porexemplo,

merge [2,4,6,8] [1,2,3,5,7] = [1,2,2,3,4,5,6,7,8]

4.16 Defina a funcao prefixos :: [a] -> [[a]] que, dada uma lista da comoresultado os prefixos dessa lista; por exemplo, prefixos [1,2,3] = [[], [1],[1,2], [1,2,3]]

4.17 Defina a funcao sufixos :: [a] -> [[a]] que, dada uma lista da como re-sultado os sufixos dessa lista. Por exemplo, sufixos [1,2,3] = [[1,2,3],[2,3], [3], []].

4.18 Redefina a funcao sufixos usando as funcoes prefixos e reverse.

4.19 Considere a seguinte definicao

func l = [(x,y) | x <- l ; y <-l]

(1) Qual o tipo de func?(2) Qual o resultado de func [1,2,3]?(3) Redefina a funcao func sem usar definicoes por compreensao.

4.20 Complete a seguinte definicao da funcao maxAc que calcula o maior elemento deuma lista nao vazia:

maxAc (h:t) = maxAC .... wheremaxAC ac [] = acmaxAC ac (x:xs)| ac > x = maxAC ...| otherwise = maxAC ...

4.21 Defina uma funcao maisLonga :: [[a]] -> [a] que retorna a lista maislonga de uma lista nao vazia de listas.

4.22 Compare as performances da funcao que definiu no exercıcio anterior com o daseguinte funcao

maisLonga (l:ls) =fst (maisLAc (l,(length l)) ls) wheremaisLAc (l,c) [] = (l,c)maisLAc (l,c) (l1:ls) =let c1 = length l1 in

Page 58: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

54 4. LISTAS

if (c > c1)then maisLAc (l,c) lselse maisLAc (l1,c1) ls

4.23 Considere a seguinte definicao da funcao junta :: [[a]] -> [a] que juntanuma so sequencia uma lista de sequencias:

junta [] = []junta (l:ls) = l ++ ls

Nesta definicao, a reducao de junta [l1,l2,...ln] vai corresponder a calcular

l1 ++ (l2 ++ (... ++ (ln ++ [])...))

Usando acumuladores, defina uma funcao juntaAc de forma a que a reducao dejuntaAc [l1,l2,...ln] corresponda a calcular

((...(([] ++ l1) ++ l2) ++ ...) ++ ln)

Qual das duas definicoes e mais eficiente? Use o hugs para cofirmar a suaresposta.

4.24 Complete a seguinte definicao da funcao que calcula a media dos elementos deuma lista (nao vazia).

media :: [Float] -> Floatmedia (x:xs) = mediaAC .... wheremediaAC ac [] = ....mediaAC (n,s) (h:t) = mediaAC (n+1,s+h) t

Page 59: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

CAPITULO 5

Ordenacao de listas

Uma das classes de problemas mais caracterısticas num curso de iniciacao a pro-gramacao e o da ordenacao. O que se pretende e definir uma funcao que dada umalista, de como resultado essa lista ordenada. Ha muitos processos (algoritmos) paradefinir tal operacao.Vejamos entao como a receita que temos vindo a usar para definir funcoes sobre listas,nos pode ajudar a obter uma primeira definicao.Para obtermos tal definicao devemos responder as duas questoes habituais: qual oresultado de ordenar uma lista vazia, e, partindo do princıpio que conhecemos o re-sultado de ordenar a lista xs, como e que construımos o resultado de ordenar a lista(x:xs). Da resposta a estas duas perguntas obtemos a seguinte definicao:

isort [] = []isort (x:xs) = insert x (isort xs)

Para que esta definicao esteja correcta e necessario que a funcao insert calcule umalista ordenada, a partir de um elemento e de uma lista ordenada. Para escrevermos adefinicao desta funcao podemos voltar a usar a receita habitual

insert a [] = [a]insert a (b:bs)| (a <= b) = a:b:bs| otherwise = b:(insert a bs)

Vejamos um exemplo de calculo com esta definicao:

isort [3,2,1]

=⇒ insert 3 (isort[2,1])

=⇒ insert 3 (insert 2 (isort[1]))

=⇒ insert 3 (insert 2 (insert 1 (isort[])))

=⇒ ...=⇒ insert 3 (1:[2])

=⇒ 1:(insert 3 [2])

=⇒ 1:2:(insert 3 [])

=⇒ 1:2:[3]

55

Page 60: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

56 5. ORDENACAO DE LISTAS

Esta definicao e normalmente conhecida por ordenacao por insercao ordenada.

Uma outra estrategia para ordenar uma lista consiste em determinar o menor dos seuselementos (que sera o primeiro elemento do resultado da ordenacao) e ir repetindoeste metodo aos restantes elementos da lista.

minsort [] = []minsort (x:xs) = menor:(minsort outros) where(menor:outros) = calcMenor (x:xs)

A funcao calcMenor deve retornar a lista com o seu menor elemento a cabeca.

calcMenor [a] = [a]calcMenor (a:b:bs) =let (menor:outros) = calcMenor (b:bs)inif (a<menor)then (a:menor:outros)else (menor:a:outros)

Ambos os metodos acima sao quadraticos no comprimento da lista. Se multiplicarmoso comprimento da lista por um factor φ, o numero de reducoes necessarias ao calculodo resultado vem multiplicado por φ2. Podemos usar o hugs para evidenciar estefacto.

Ordenacao> :set +s

Ordenacao> minsort [10,9..1]

[1,2,3,4,5,6,7,8,9,10]

(761 reductions, 1553 cells)

Ordenacao> minsort [100,99..1]

...

(52053 reductions, 108521 cells)

Ordenacao> minsort [1000,999..1]

...

(5020053 reductions, 10535472 cells, 45 garbage collections)

Ordenacao> isort [10,9..1]

[1,2,3,4,5,6,7,8,9,10]

(723 reductions, 1005 cells)

Ordenacao> isort [100,99..1]

...

(51753 reductions, 67921 cells)

Ordenacao> isort [1000,999..1]

...

(5017053 reductions, 6529472 cells, 27 garbage collections)

Page 61: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

5. ORDENACAO DE LISTAS 57

Este teste usa realmente o pior caso possıvel que corresponde a termos a lista originalordenada por ordem decrescente.Vejamos agora o caso mais favoravel, em que a lista ja se encontra ordenada:

Ordenacao> minsort [1..10]

[1,2,3,4,5,6,7,8,9,10]

(801 reductions, 1617 cells)

Ordenacao> minsort [1..100]

...

(52633 reductions, 109395 cells)

Ordenacao> minsort [1..1000]

...

(5026033 reductions, 10544446 cells, 45 garbage collections)

Ordenacao> isort [1..10]

[1,2,3,4,5,6,7,8,9,10]

(394 reductions, 601 cells)

Ordenacao> isort [1..100]

...

(3724 reductions, 5732 cells)

Ordenacao> isort [1..1000]

...

(37024 reductions, 57933 cells)

Este caso evidencia que o algoritmo de insercao ordenada tem muito melhor perfor-mance – e linear no tamanho do argumento, isto e, aumentando o comprimento dalista 100 vezes, o numero de reducoes aumenta tambem 100 vezes.Por sua vez a funcao minsort continua quadratica – aumentando o tamanho do ar-gumento 100 vezes, o numero de reducoes aumenta 10000 vezes!

O calculo da eficiencia dos algoritmos de ordenacao que foi apresentado acima foi feitade uma forma muito informal. Existe uma extensa teoria acerca deste assunto e talnao cabe de todo num curso como este de introducao a programacao1. Faz no entantoalgum sentido que se comece a incentivar o leitor para o facto de que pode haverformas substancialmente mais eficientes de atingir um mesmo objectivo. Foi dentrodesta linha que apresentamos o uso de acumuladores.Uma outra estrategia de conseguir algoritmos mais eficientes e conhecida por divideand conquer2. A ideia base desta estrategia consiste em comecar por dividir o problemaem causa em problemas identicos mas aplicados a argumentos substancialmente maissimples. De seguida tenta-se combinar as varias solucoes destes sub-problemas nasolucao do problema inicial.Talvez o algoritmo de ordenacao que melhor traduz esta estrategia seja o algoritmode merge sort (uma traducao desta designacao seria insercao por fusao). Assim, e

1Ver por exemplo [].2Cuja traducao literal e Dividir e conquistar e que e provavelmente uma heranca maquiavelica

nas ciencias de computacao.

Page 62: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

58 5. ORDENACAO DE LISTAS

para ordenar uma lista segundo este algoritmo, comeca-se por dividir a lista em duas.Depois de ambas estas listas estarem ordenadas (pelo mesmo processo) devemos fundir(merge) as duas listas ordenadas numa unica lista ordenada. E claro que este processoso pode ser aplicado a listas com pelo menos dois elementos.A definicao seguinte espelha a descricao feita.

mergesort [] = []mergesort [a] = [a]mergesort lista = merge (mergesort l1) (mergesort l2)where (l1,l2) = split lista

A funcao split divide uma lista em duas e a funcao merge funde duas listas ordenadasnuma unica lista ordenada.Comecemos entao por definir a fusao de listas ordenadas.

merge [] lista2 = lista2merge lista1 [] = lista1merge (x:xx) (y:yy)| x < y = x: (merge xx (y:yy)| otherwise = y:(merge (x:xx) yy)

Alternativamente, e sabendo que quando usamos padroes nao exclusivos estes saoescolhidos pela ordem em que aparecem, podemos apresentar esta definicao de umaforma mais compacta.

merge (x:xx) (y:yy)| x < y = x: (merge xx (y:yy)| otherwise = y:(merge (x:xx) yy)

merge lista1 lista2 = lista1 ++ lista2

Note-se contudo que esta definicao e menos eficiente do que a primeira3.A funcao split deve fazer uma divisao equitativa da lista original em duas. Umaforma de fazer essa divisao e comecar por calcular o comprimento da lista em causa eso depois fazer a divisao da lista (usando a funcao splitAt que parte uma lista numadeterminada posicao):

split l = splitAt ((length l) ‘div‘2) l

Esta solucao tem contudo o defeito de ter de percorrer a lista duas vezes: primeiropara calcular o seu comprimento e depois para a dividir. Vejamos como atingir esteobjectivo com uma unica travessia da lista:

3Use o hugs para comparar o numero de reducoes no calculo de merge [1..100] [] em ambasas alternativas.

Page 63: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

5. ORDENACAO DE LISTAS 59

split [] = ([],[])split [a] = ([a],[])split (a:b:bb) = (a:xx,b:yy)where (xx,yy) = split bb

Uma forma alternativa de definir esta mesma funcao e:

split [] = ([],[])split (a:aa) = (a:yy,xx)where (xx,yy) = split aa

A primeira vista pode parecer que esta funcao apenas coloca elementos no primeiroelemento do par. Uma analise mais cuidada revela no entanto que a ordem por queas duas listas aprecem no par vai sendo trocada. Daı que o par de listas resultantetenha de facto duas listas de comprimentos iguais (a menos de uma unidade no casode a lista original ter um numero ımpar de elementos).

De seguida apresentamos uma analise grafica do funcionamento desta funcao. A partede analise, i.e. de divisao em problemas cada vez mais simples, pode ser vista noseguinte diagrama:

[5,4,7,2] [1,6,3,8]

[5,4] [7,2] [1,6] [3,8]

[5] [4] [2] [7] [6] [1] [8] [3]

[5,4,7,2,1,6,3,8]

split

split

split split

split

split split

Note-se que nesta altura temos um conjunto de listas unitarias que, por definicao,estao ordenadas. Devemos comecar entao a construir solucoes de problemas maiscomplexos a partir destas solucoes simples. Para isso vamos fazendo a fusao dasvarias listas ordenadas.

Page 64: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

60 5. ORDENACAO DE LISTAS

[1,2,3,4,5,6,7,8]

merge

[7]

[2,4,5,7]

[2]

[2,7]

[5] [4]

[4,5]

merge

merge

merge

[3]

[1,3,6,8]

[8]

[3,8]

[6] [1]

[1,6]

merge

merge

merge

Dentro da classe de algoritmos de divide and conquer para ordenar listas, talvez omais famoso seja o quicksort. Aqui a divisao faz-se comecando por seleccionar umelemento da lista (o pivot) e depois dividir a lista original em duas sub-listas: a dosque sao menores do que o pivot, e a dos outros elementos. A fase de sıntese destealgoritmo corresponde a concatenar as varias solucoes intermedias de forma a obter asolucao final.

qsort [] = []qsort lista =let (pivot, resto) = escolhePivot lista

(menores,maiores) = split pivot restoin join (qsort menores) pivot (qsort maiores)

Tal como no merge sort o sucesso da aplicacao desta tecnica esta relacionado com con-seguirmos dividir o problema inicial em sub-problemas substancialmente mais simples.Por isso mesmo, o caso em que este algoritmo e mais eficaz e quando conseguimos de-terminar como pivot o elemento de valor medio – dessa forma os dois sub-problemasobtidos sao de facto substancialmente menores do que o original (tem metade dotamanho).Se esta divisao equitativa era facilmente alcancavel no merge sort – com uma funcaolinear (i.e., que precorria a lista uma unica vez), tal nao e possıvel no quicksort. Assimsendo, a escolha do pivot faz-se de uma forma aleatoria. De forma a optimizarmosesta operacao vamos escolher como pivot o primeiro elemento da lista.

escolhePivot (x:xx) = (x,xx)

Page 65: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

5. ORDENACAO DE LISTAS 61

Vejamos entao um exemplo – a ordenacao da lista [4,2,6,1,5,1,7,3]. Como veremoseste exemplo foi especialmente escolhido para que a escolha do primeiro elemento dalista como pivot seja de facto a escolha optima. Tal nao acontece em geral.A primeira fase, de analise corresponde a ir dividindo o problema inicial em problemassubstancialmente mais simples.

[4,2,1,6,5,7,3]

[2,1,3]

split

4 [6,5,7]

split

[1]

split

2 [3] [5] [7]6

A funcao split recebe como argumento o pivot e uma lista, e retorna um par de listas– os elementos da lista menores do que o pivot e os restantes elementos.

split x l = ([a | a <- l, a < x], [a | a <- l , a >= l])

Novamente podemos constatar que esta solucao implica uma dupla travessia da lista.Mas este mesmo objectivo pode ser atingido com uma unica travessia.

split [] x = ([],[])split (a:as) x =let (men,mai) = parte as xinif (a < x)then (a:men,mai)else (men,a:mai)

A segunda fase, de sıntese, vai construindo a solucao de problemas cada vez maiscomplexos a partir das solucoes dos problemas simples. Note-se que a primeira fasetermina com problemas cuja solucao e trivial – neste caso a ordenacao de listas sin-gulares.

Page 66: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

62 5. ORDENACAO DE LISTAS

6

join

[5,6,7]

2

4

[5] [7][1] [3]

join

[1,2,3]

[1,2,3,4,5,6,7]

join

A funcao de sıntese – join – e muito simples pois tem apenas de concatenar as duaslistas e o pivot na ordem correcta.

join l1 p l2 = l1 ++ [p] ++ l2

Convem realcar que o sucesso das estrategias de divide and conquer esta baseado emconseguir que a dimensao dos sub-problemas em que se decompoe o problema origi-nal seja substancialmente menor do que a desse problema. Em ambos os algoritmosdescritos aqui, tenta-se obter sub-problemas com metade do tamanho. Sem esta pre-ocupacao, estes dois algoritmos seriam exactamente iguais aos que comecamos porapresentar. Senao vejamos:

• Se a funcao split do algoritmo de merge sort der como resultado uma listacom um elemento (a cabeca da lista) e outra com os restantes elementos (acauda), este algoritmo degenera no algoritmo de insercao ordenada – fazer afusao (merge) de uma lista unitaria com outra nao e nada mais do que fazera insercao ordenada de um elemento numa lista ordenada.• Se o metodo de escolha do pivot no quicksort for escolher o menor dos ele-

mentos da lista, este algoritmo degenera no algoritmo de minsort.

Exercıcios

5.1 Um dos defeitos do algoritmo de merge sort e que o seu comportamento naodepende do estado original da lista – se esta estiver quase ordenada, a fase deanalise encarrega-se de destruir essa caracterıstica. De forma a minorar estedefeito pode-se pensar numa variante deste algoritmo em que a fase de analise

Page 67: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

5. ORDENACAO DE LISTAS 63

termine com as sublistas ja ordenadas da lista original. Defina esta variante domerge sort.

5.2 Uma versao da funcao parte (do algoritmo de quicksort) usando acumuladorese.

split’ x l = splitAC x ([],[]) lwhere splitAC x (men,mai) [] = (men,mai)

splitAC x (men,mai) (h:t)| (h <= x) = splitAC x (h:men,mai) t| otherwise = splitAC x (men,h:mai) t

No entanto estas duas funcoes nao dao o mesmo resultado.(1) Qual o resultado de split 5 [1..10]?(2) Qual o resultado de split’ 5 [1..10]?(3) Explique porque e que se pode usar a funcao split’ em vez da funcao

split na funcao qsort.

Page 68: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao
Page 69: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

CAPITULO 6

Funcoes de ordem superior

Dissemos atras que em Haskell, todas as funcoes tinham um unico argumento eretornavam um unico resultado. Por isso, quando pretendıamos escrever a definicaode uma funcao binaria (i.e., com dois argumentos), existiam duas alternativas: definira funcao usando como argumento um par ou usar a versao curried dessa funcao.Por exemplo, a funcao snoc que recebe como argumentos uma lista e um elemento eretorna a lista resultante de acrescentar o elemento no fim da lista, pode ser definidacomo se segue, usando a primeira alternativa.

snoc1 :: (a,[a]) -> [a]snoc1 (x,xs) = xs ++ [x]

Ou, da seguinte forma, usando a versao curried.

snoc2 :: a -> ([a] -> [a])snoc2 x xs = xs ++ [x]

Atentemos melhor no tipo de snoc2. Tal como com snoc1, a funcao da um unicoresultado – so que neste ultimo caso esse resultado e, por sua vez, uma funcao. Porexemplo, a funcao snoc2 5 e a funcao que, dada uma lista, retorna essa lista com onumero 5 no fim.Esta capacidade que muitas linguagem oferecem de nos podermos referir a entidades– neste caso funcoes – que trabalham sobre outras entidades do mesmo tipo, conferea linguagem o atributo conhecido por ordem superior1.Um dos primeiros contactos que tivemos com funcoes de ordem superior neste livroforam as funcoes curry e uncurry. Estas funcoes sao usadas para obter as versoescurried e uncurried de uma funcao. A definicao destas funcoes e.

curry f x y = f (x,y)

uncurry f (x,y) = f x y

1Este conceito, que pode parecer bastante confuso, e bastante comum no dia-a-dia de cada umde nos – quantos de nos nunca terao pensado no que e pensar?

65

Page 70: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

66 6. FUNCOES DE ORDEM SUPERIOR

Ambas estas funcoes recebem como primeiro argumento uma funcao. Podemos usarum interpretador de Haskell para determinar os tipos destas funcoes.

Prelude> :t curry

curry :: ((a,b) -> c) -> a -> b -> c

Prelude> :t uncurry

uncurry :: (a -> b -> c) -> (a,b) -> c

Por exemplo, a funcao div tem tipo Int -> Int -> Int. Para calcular a divisaointeira de 100 por 13 usamos a expressao div 100 13. Usando a versao uncurrieddesta funcao teremos (uncurry div) (100,13).

Uma outra funcao de ordem superior bastante familiar e a composicao de funcoes. Deuma forma geral podemos dizer que, se (f :: a -> b) e (g :: b -> c), entao afuncao (g ◦ f :: a -> c) (e e costume ler-se g apos f) define-se como

g ◦ f x = g (f x)

Podemos definir este comportamento em Haskellde seguinte forma.

compoe g f x = g (f x)

Note-se que esta funcao recebe como argumento duas funcoes. O seu tipo e

compoe :: (b -> c) -> (a -> b) -> a -> c

que, escrito desta forma (i.e., usando a convencao de parentesis referida na pagina 9),pode ser lido como a funcao compoe aceita como parametros uma funcao de a para b,uma funcao de b para c e um elemento de a e retorna um elemento de c.E de notar ainda que a funcao compoe existe pre-definida em Haskell e e umaoperacao infixa. Em vez de escrevermos compoe f g, basta-nos escrever f . g.

A funcao flip :: (a -> b -> c) -> (b -> a -> c) recebe uma funcao (com doisargumentos) e retorna essa funcao com a ordem dos seus argumentos trocada; porexemplo, flip div e a funcao que retorna o quociente entre o segundo e o primeiroargumento.

Main> (flip div) 2 10

5

Main>

A funcao flip define-se por

flip :: (a -> b -> c) -> (b -> a -> c)flip f x y = f y x

A definicao do tipo desta funcao parece nao corresponder a da definicao da funcao pro-priamente dita – enquanto que no tipo afirmamos que a funcao so tem um argumento,na definicao da funcao fornecemos-lhe tres. Esta discrepancia pode ser resolvida se

Page 71: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

6. FUNCOES DE ORDEM SUPERIOR 67

recordarmos a convencao de parentesis apresentada atras. Se usarmos tal convencaoe eliminarmos os parentesis desnecessarios, o tipo desta funcao escreve-se como

flip :: (a -> b -> c) -> b -> a -> c

estando entao mais de acordo com a definicao apresentada.

A definicao de funcoes de ordem superior surge muitas vezes como uma forma dereutilizar funcoes que tinham sido desenvolvidas para um fim especıfico mas que depoisse constata poderem ser usadas noutros contextos.Vejamos um exemplo. Pretende-se definir uma funcao que calcule o comprimento damaior sequencia ascendente de uma lista. Para isso vamos comecar por definir umafuncao que dada uma lista, calcule todas as suas sub-sequencias ascendentes. Umaanalise semelhante a que foi feita para outros problemas leva-nos a seguinte solucao.

ascSeq [] = []ascSeq [x] = [[x]]ascSeq (x:xx) =let (yy:yyy) = ascSeq xxinif (x <= (head yy))then (x:yy):yyyelse [x]:(yy:yyy)

Com esta funcao, a solucao do problema inicial e muito simples.

maiorSSAsc l = maximum [length ss | ss <- ascSeq l]

Podemos agora pensar noutros problemas semelhantes que terao solucoes muito proximas:

• comprimento da maior sequencia descendente de uma lista;• comprimento da maior sequencia de elementos iguais;• comprimento da maior sequencia de elementos consecutivos.• . . .

Em todas estas variantes, aquilo que temos que fazer e uma funcao semelhante afuncao ascSeq mudando apenas o teste que se efectua entre os elementos da lista.Podemos por isso definir uma funcao em que esse proprio teste seja um dos argumentosda funcao. Esta funcao (de ordem superior) agrupa pode ser vista como abstraccaoda funcao ascSeq:.

agrupa :: (a -> a -> Bool) -> [a] -> [[a]]agrupa p [] = []agrupa p [x] = [[x]]agrupa p (x:xx) =let (yy:yyy) = agrupa p xx

Page 72: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

68 6. FUNCOES DE ORDEM SUPERIOR

inif (p x (head yy))then (x:yy):yyyelse [x]:(yy:yyy)

Note-se que de facto se trata de uma abstraccao da funcao ascSeq – esta ultima podeser obtida simplesmente por.

ascSeq = agrupa (<=)

Alem disso, as variantes que enumeramos acima podem ser facilmente escritas usandoesta funcao:

• comprimento da maior sequencia descendente de uma lista lmaximum [length ss | ss <- agrupa (>=) l]• comprimento da maior sequencia de elementos iguais de uma lista lmaximum [length ss | ss <- agrupa (==) l]• comprimento da maior sequencia de elementos consecutivos de uma lista lmaximum [length ss | ss <- agrupa (\x y -> y==x+1) l]

Como vimos na definicao da funcao agrupa, o uso de funcoes de ordem superioresta muitas vezes ligado a constatacao da ocorrencia de certos padroes na definicaode funcoes. E nesta perspectiva que vamos apresentar duas das funcoes de ordemsuperior mais comuns.Reparemos nas seguintes definicoes.

vDois :: [Int] -> [Int]vDois [] = []vDois (x:xs) = (2*x):(vDois xs)

arr :: [Float] -> [Int]arr [] = []arr (x:xs) = (round x):(arr xs)

A estrutura destas duas funcoes e muito semelhante. Trata-se de funcoes que recebemuma lista como argumento e retornam uma lista. Em ambos os casos o valor retornadopelas funcoes e obtido a partir da aplicacao sistematica de uma dada transformacaoa cada um dos elementos da lista argumento – no primeiro caso, multiplicam-se por2 enquanto que no segundo caso aplica-se a funcao round que arredonda um numerode vırgula flutuante para o inteiro mais proximo.Podıamos, por isso, tentar descrever um metodo para a construcao deste tipo defuncoes.

Dada uma funcao f :: a -> b, para definirmos a funcao queaplica f a todos os elementos de uma lista escrevemos

Page 73: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

6. FUNCOES DE ORDEM SUPERIOR 69

aplicaf [] = []aplicaf (x:xs) = (f x):(aplicaf xs)

Para que esta funcao possa ser aplicada a uma lista, os elementos dessa lista tem queser do tipo a. Por outro lado, os elementos da lista resultante desta aplicacao vao serdo tipo b. Por outras palavras, o tipo da funcao aplicaf e [a] -> [b].Note-se que, ao escrevermos a definicao da funcao aplicaf, estamos a assumir queexiste uma funcao f em particular. Podemos entao escrever uma definicao que naoparte deste princıpio – em vez disso, a funcao f sera fornecida como parametro:

aplica f [] = []aplica f (x:xs) = (f x):(aplica f xs)

E qual sera entao o tipo da funcao aplica? Como vimos atras, se assumıssemos quef::a -> b, podıamos concluir que o tipo de aplicaf e aplicaf :: [a] -> [b].Daı que, ao incorporarmos esta funcao como argumento de aplica, passamos a teruma funcao de ordem superior

aplica :: (a->b) -> [a] -> [b]

Ou, se colocarmos os parentesis implıcitos,

aplica :: (a->b) -> ([a] -> [b])

Este tipo pode ter duas leituras possıveis:

• a funcao aplica tem dois argumentos em que o primeiro e, ele proprio, umafuncao, e o segundo e uma lista;• a funcao aplica tem um argumento e um resultado ambos funcoes.

A funcao aplica existe pre-definida em Haskell com o nome de map.Podemos com esta funcao reescrever as duas definicoes acima.

vDois l = map (2*) larr l = map round l

Recorde que a expressao (2*) e a funcao que multiplica o seu argumento por 2 (eestamos agora a comecar a colher os frutos de usarmos as versoes curried da operacaode multiplicacao).Note-se ainda que, com o uso da funcao aplica, como que escondemos o facto de asfuncoes vDois e arr serem recursivas – a parte recursiva esta toda concentrada no usode map.

Page 74: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

70 6. FUNCOES DE ORDEM SUPERIOR

Vejamos, por fim, um calculo (i.e., uma reducao) que envolve estas funcoes.

vDois [1,2,3]

=⇒ map (2*) [1,2,3]

=⇒ (2*1):(map (2*) [2,3])

=⇒ (2*1):((2*2):(map (2*) [3]))

=⇒ (2*1):((2*2):((2*3):(map (2*) [])))

=⇒ (2*1):((2*2):((2*3):[]))

=⇒ . . .=⇒ 2:(4:(6:[]))

Convem ainda referir a estreita relacao que existe entre a funcao map e a facilidadede definir listas por compreensao apresentada atras. Uma outra forma de definir afuncao vDois e

vDois l = [2*x | x <- l]

De uma forma geral pode-se mostrar que map f l = [f x | x <- l].Ainda relacionada com as definicoes por compreensao existe uma outra funcao deordem superior – a funcao filter que selecciona os elementos de uma lista que satis-fazem um dado predicado. A definicao desta funcao e bastante simples.

filter :: (a -> Bool) -> [a] -> [a]filter p [] = []filter p (x:xs)| (p x) = x:(filter p xs)| otherwise = filter p xs

Vejamos entao um exemplo: como determinar de entre os elementos de uma lista osque sao multiplos de 3. Uma primeira aproximacao, sem usar a funcao filter seria

mult3 [] = []mult3 (x:xs)| ((mod x 3) == 0) = x:(mult3 xs)| otherwise = (mult3 xs)

Note-se a semelhanca estrutural entre esta definicao e a definicao da funcao filter.Usando esta funcao, conseguimos esconder a parte comum a estas duas definicoes.

mult3 l = filter multiplode3 lwhere multiplode3 x = (mod x 3) == 0

Esta mesma definicao podia ser feita usando listas por compreensao.

mult3 l = [x | x <- l , (mod x 3) == 0]

Page 75: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

6. FUNCOES DE ORDEM SUPERIOR 71

Vejamos agora um exemplo de uso de funcoes de ordem superior na definicao de umalgoritmo famoso para o calculo dos numeros primos menores do que um dado numero.Este algoritmo e conhecido por crivo de Eratostenes e veremos como o uso de funcoesde ordem suerior pode facilitar e clarificar as definicoes de outras funcoes.Este algoritmo pode ser descrito da seguinte forma:.

Para calcular os numeros primos menores do que n, comeca-se porconstruir o conjunto de todos os numeros inteiros desde 2 ate n.Chamemos a este conjunto C. De seguida executam-se os seguintespassos ate que nao existam mais elementos em C:(1) escolhe-se o menor elemento p de C – esse numero e primo;(2) retira-se de C o elemento p bem como todos os multiplos de p.

Para determinar se um dado numero x nao e multiplo de p podemos construir a funcaonaoMultiplo:

naoMultiplo p x = (mod x p) /= 0

Para eliminar todos os multiplos de p de uma lista basta escrever

eliminaMultiplos p l = filter (naoMultiplo p) l

A funcao que calcula todos os numeros primos menores que um dado numero podeentao ser definida por

primosAte n = crivo [2..n]

crivo [] = []crivo (h:t) = h:(crivo (eliminaMultiplos h t))

Ou, juntando tudo numa mesma definicao,

primosAte n = crivo [2..n]

crivo [] = []crivo (h:t) = h:(crivo (eliminaMultiplos h t))whereeliminaMultiplos p l = filter (naoMultiplo p) lnaoMultiplo p x = (mod x p) /= 0

Exercıcios

6.1 Considere a seguinte funcao:

Page 76: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

72 6. FUNCOES DE ORDEM SUPERIOR

fun l = [3 * x | y <- l , x <- y]

(1) Qual o tipo de fun?(2) Reescreva a definicao de fun usando a funcao map em vez da definicao de

listas por compreensao.

6.2 Para cada uma das expressoes seguintes, determine o seu valor e reescreva-ausando listas por compreensao.(1) map succ [5,6,23,3](2) map odd [1,2,3,4,5](3) map succ (filter odd [1,2,3,4,5])(4) filter odd (map succ [1,2,3,4,5])(5) map (3+) [1..15](6) map (7>) [1..15](7) filter (7<) [1,3..15](8) map (7:) [[2,3],[1,5,3]](9) map (:[]) [1..5]

6.3 Considere as seguintes definicoes

type numAluno = Inttype numTurno = Inttype NomAluno = Stringtype NomDisciplina = Stringtype Aluno = (NumAluno,NomAluno)type Turno = (NumTurno,[Aluno])type Disciplina = [Turno]type Ano = [(NomDisciplina,Disciplina)]type Curso = (NomCurso,[Ano])

pfunc :: Disciplinapfunc = [(1,[(1234,"Chico"),(2345,"Maria"),(3456,"Manel")]),(3,[(7282,"Jorge"),(7984,"Carlos"),(7458,"Ana")]),(2,[(6464,"Joana"),(6326,"Zulmira"),(6470,"Joao")]),(8,[(174,"Elsa"),(1055,"Cristina"),(1234,"Chico")])]

Usando as funcoes map e filter, defina funcoes que permitam determinar:(1) Quais as disciplinas do ano?(2) Dada uma disciplina D, quantos alunos estao inscritos a essa disciplina?(3) Dado um ano A e uma disciplina D, quantos alunos estao inscritos a essa

disciplina?(4) Dado um ano A, quais os alunos que estao a frequentar esse ano e cujo

numero de aluno esta entre 1000 e 2000?

Page 77: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

6. FUNCOES DE ORDEM SUPERIOR 73

(5) Dado um curso C e um numero de aluno, quais as disciplinas a que estainscrito?

6.4 Determine uma funcao um :: Int -> Int -> Bool tal que

agrupa um [1,2,3,6,7,2,3,3] = [[1],[2],[3],[6],[7],[2],[3],[3]]

6.5 Defina uma funcao conta :: [a] -> [(a,Int)] que conta as ocorrenciasrepetidas de elementos numa lista; por exemplo,

conta [1,1,1,4,5,5,1,1] = [(1,3),(4,1),(5,2),(1,2)]

6.6 Defina uma funcao desconta:: [(a,Int)] -> [a] que inverte o efeito dafuncao conta. Por exemplo,

desconta [(1,3),(4,1),(5,2),(1,2)] = [1,1,1,4,5,5,1,1]

6.7 Usando a funcao words, defina agora uma funcao cref:: String -> [(String,Int)]que conte o numero de ocorrencias de cada palavra num texto; por exemplo, cref"o tempo perguntou ao tempo quanto tempo o tempo tem" deve dar comoresultado a lista [("ao",1), ("o",2), ("perguntou",1), ("quanto",1),("tem",1), ("tempo",4)].

6.8 Usando a funcao anterior, defina uma funcao que dado um texto, determine apalavra que ocorre mais vezes nesse texto.

Page 78: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao
Page 79: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

CAPITULO 7

Travessia de listas

Tal como na motivacao da funcao map, vamos apresentar dois exemplos muito semel-hantes para motivar a definicao de uma outra funcao de ordem superior – a funcaofoldr.Considerem-se entao as seguintes definicoes.

soma :: [Int] -> Intsoma [] = 0soma (x:xs) = x + (soma xs)

prod :: [int] -> Intprod [] = 1prod (x:xs) = x * (prod xs)

Em ambos os casos, o que estamos a fazer e a extensao de uma operacao binaria auma lista de operandos. Aquilo que e especıfico de cada uma destas definicoes e:

• o resultado da operacao quando a lista de operandos e vazia;• a operacao propriamente dita.

Vamos definir uma funcao de ordem superior que resuma a parte comum a estasduas definicoes. Essa funcao existe em Haskell com o nome de foldr e recebe doisparametros (para alem da lista a que vai ser aplicada): a operacao binaria e o resultadoque deve ser produzido se a lista for vazia.

foldr f e [] = efoldr f e (x:xs) = f x (foldr f e xs)

As funcoes soma e prod podem agora ser definidas como:

• soma l = foldr (+) 0 l• prod l = foldr (*) 1 l

Muitas das funcoes (recursivas) sobre listas apresentadas atras podem ser definidasusando foldr. Por exemplo as funcoes map e length podem ser definidas dessa forma.

• length l = foldr (\x y -> 1+y) 0 l• map f l = foldr (\x y -> (f x):y) [] l

Se usarmos a definicao de foldr para reduzir a expressao foldr (+) 0 [1..5], obte-mos

1 + (2 + (3 + (4 + (5 + 0))))

75

Page 80: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

76 7. TRAVESSIA DE LISTAS

De uma forma geral, temos que, para um operador infixo �,

foldr � z [x1, ..., xn] = x1 � (x2 � (· · · � (xn � z) · · · ))

Dizemos, por isto mesmo, que ao aplicar uma operacao binaria a uma lista de elementosa funcao foldr faz associacao a direita (right). Daı que a operacao se chame foldr.Tanto no caso do produtorio como do somatorio, a ordem pela qual se fazem asoperacoes elementares (a multiplicacao e a adicao) nao e relevante. Poderıamos terdefinido estas operacoes com uma associacao a esquerda (left). Para isso devemos usarantes a funcao de ordem superior foldl. Esta funcao e tal que, para um operadorinfixo �,

foldl � z [x1,...,xn] = (· · · ((z � x1)� x2)� · · · )� xn

Por exemplo, a expressao foldl (+) 0 [1..5] corresponde a

((((0 + 1) + 2) + 3) + 4) + 5

A definicao de foldl e

foldl f e [] = efoldl f e (x:xs) = foldl f (f e x) xs

Como dissemos atras, tanto a adicao como a multiplicacao sao associativas, pelo que,podemos definir as operacoes de somatorio e produtorio usando foldl:

• soma l = foldl (+) 0 l• prod l = foldl (*) 1 l

Para melhor nos apercebermos das diferencas entre estas duas funcoes, vejamos umexemplo de reducao de cada uma delas.

foldr (+) 0 [1,2,3]

=⇒ 1+(foldr (+) 0 [2,3])

=⇒ 1+(2+(foldr (+) 0 [3]))

=⇒ 1+(2+(3+(foldr (+) 0 [])))

=⇒ 1+(2+(3+0))

=⇒ ...

=⇒ 6

foldl (+) 0 [1,2,3]

=⇒ foldl (+) (0+1) [2,3]

=⇒ foldl (+) ((0+1)+2) [3]

=⇒ foldl (+) (((0+1)+2)+3) []

=⇒ (((0+1)+2)+3)

=⇒ ...

=⇒ 6

Este exemplo poe em evidencia, nao so a diferenca entre as duas versoes da definicao dosomatorio, mas principalmente, relaciona as funcoes foldr e foldl com as estrategiasque definimos atras para construir funcoes recursivas. Existe uma relacao muito forteentre:

• a funcao foldr e a definicao de funcoes sobre listas por recursividade prim-itiva – os dois argumentos desta funcao correspondem exactamente as re-spostas que tınhamos que fornecer para escrever as definicoes recursivas;

Page 81: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

7. TRAVESSIA DE LISTAS 77

• a definicao de funcoes usando acumuladores e o uso da funcao foldl (verpagina 49) – os dois parametros passados a funcao foldl correspondem aovalor inicial do acumulador e a forma de actualizar o acumulador.

Existe definida em Haskell variantes destas duas funcoes que so estao definidas paralistas nao vazias. Trata-se das funcoes foldr1 e foldl1.Vejamos como definir as funcoes que calculam o maior e menor elementos de umalista. Definicoes possıveis destas funcoes sao.

minimum [x] = xminimum (x:(y:ys)) = min x (minimum (y:ys))

maximum [x] = xmaximum (x:(y:ys)) = max x (maximum (y:ys))

Estas funcoes nao podem ser definidas imediatamente com a funcao foldr pois naoestao definidas para listas vazias – e a funcao foldr devemos fornecer o resultado dea aplicar a uma lista vazia. Podemos no entanto usar um pequeno artifıcio: aplicarfoldr a cauda da lista, usando o primeiro elemento como resultado no caso da listaso ter esse elemento. Tais funcoes podem ser escritas como

minimum (x:xs) = foldr min x xs

maximum (x:xs) = foldr max x xs

A variante das funcoes foldr e foldl sao as versoes destas funcoes que estao definidasapenas para listas nao vazias. As definicoes destas funcoes sao

foldr1 f (x:xs) = foldr f x xs

foldl1 f (x:xs) = foldl f x xs

Usando estas funcoes podemos definir o calculo do maior e menor elementos de umalista como

maximum l = foldl1 max lminimum l = foldl1 min l

A famılia de funcoes scan esta fortemente relacionada com a famılia de funcoes queacabamos de definir.Como vimos atras, as funcoes da famılia fold permitiam-nos construir um resultadoa partir da sucessiva aplicacao de uma dada funcao a todos os elementos de umalista. No caso das funcoes da famılia scan, o resultado nao e um unico valor, mas

Page 82: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

78 7. TRAVESSIA DE LISTAS

os sucessivos resultados parciais. Por outras palavras e aplicando ao exemplo do casofoldr, para um operador infixo �, temos

foldr � z [x1, ..., xn] = x1 � (x2 � (· · · � (xn � z) · · · ))

A funcao scanr vai retornar todos os valores intermedios, i.e.,

scanr � z [x1,...,xn] = [ x1 � (x2 � (· · · � (xn � z) · · · )), x2 � (· · · � (xn � z) · · · ), · · ·, xn � z, z]

A definicao desta funcao e

scanr f z [] = [z]scanr f z (x:xs) =let (y:ys) = scanr f z xsin (f x y):(y:ys)

De igual forma para a funcao foldl, temos

foldl � z [x1,...,xn] = (· · · ((z � x1)� x2)� · · · )� xn

Para scanl temos todos os valores intermedios,

scanl � z [x1,...,xn] = [ z, z � x1

, (z � x1)� x2

, · · ·, (· · · ((z � x1)� x2)� · · · )� xn

]

A definicao desta funcao e

scanl f z [] = [z]scanl f z (x:xs) = z : (scanl f (f z x) xs)

Tambem existem as variantes destas funcoes que operam sobre listas nao vazias:

• scanr1 f (x:xs) = scanr f x xs• scanl1 f (x:xs) = scanl f x xs

Muitas das funcoes sobre listas que definimos atras podem ser generalizadas, usandofuncoes de ordem superior. Como conclusao deste capıtulo, vamos apresentar algumasdestas generalizacoes.

Page 83: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

7. TRAVESSIA DE LISTAS 79

As funcoes and e or calculam a conjuncao e disjuncao de uma lista de valores booleanos.Sendo a aplicacao de uma operacao binaria a uma lista de valores, estas funcoes

podem ser definidas usando foldr (ou foldl pois essas operacoes binarias sao asso-ciativas)

and = foldr (&&) Trueor = foldr (||) False

Estas funcoes podem ser generalizadas para, em vez de aceitarem uma lista de valoresbooleanos, aceitarem um predicado sobre um tipo a e uma lista de elementos de a.Usando listas por compreensao teremos

all p l = and [p x | x <- l]any p l = or [p x | x <- l]

Alternativamente, e sem usar listas por compreensao, temos

all p l = and (map p l)any p l = or (map p l)

Podemos escrever estas definicoes sem a explicitacao dos parametros (pointfree):

all p = and . (map p)any p = or . (map p)

A funcao zip :: [a] -> [b] -> [(a,b)] junta numa lista de pares os elementos deum par de listas. A sua definicao foi ja vista atras (pagina 48). A sua generalizacao – afuncao zipWith – em vez de agrupar os elementos das duas listas em pares, agrupa-osatraves de uma funcao que e recebida como argumento:

zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]zipWith (x:xs) (y:ys) = (f x y):(zipWith xs ys)zipWith _ _ _ = []

Ou, usando as funcoes ja definidas,

zipWith f l1 l2 = [f x y | (x,y) <- zip l1 l2]

Por exemplo, a funcao ordenada, que testa se uma lista esta ordenada, pode serdefinida por

ordenada l = and (zipWith (<=) l (tail l))

Page 84: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

80 7. TRAVESSIA DE LISTAS

As funcoes take, drop e splitAt sao usadas para seleccionar partes de uma sequencia:

• take n l corresponde aos primeiros n elementos da sequencia l

take 0 _ = []take _ [] = []take (n+1) (x:xs) = x:(take n xs)

• drop n l corresponde a sequencia resultante de retirarmos os n primeiroselementos da sequencia l:

drop 0 l = ldrop (n+1) [] = []drop (n+1) (x:xs) = drop n xs

• splitAt n l conjuga os efeitos das duas outras funcoes:

splitAt 0 l = ([],l)splitAt (n+1) [] = ([],[])splitAt (n+1) (x:xs) = (x:xs1,xs2)where (xs1,xs2) = splitAt n xs

As generalizacoes destas funcoes (respectivamente takeWhile, dropWhile e span)consistem em usar um predicado para fazer a divisao da sequencia em vez de umaposicao na lista.

takeWhile :: (a-> Bool) -> [a] -> [a]takeWhile _ [] = []takeWhile p (x:xs)| (p x) = x:(takeWhile p xs)| otherwise = []

dropWhile :: (a-> Bool) -> [a] -> [a]dropWhile _ [] = []dropWhile p (x:xs)| (p x) = dropWhile p xs| otherwise = []

span :: (a-> Bool) -> [a] -> ([a],[a])span _ [] = ([],[])span p (x:xs)| (p x) = (x:xs1,xs2)| otherwise = ([],x:xs)where (xs1,xs2) = span p xs

Page 85: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

7. TRAVESSIA DE LISTAS 81

Exercıcios

7.1 Use o mecanismo de inferencia de tipos do hugs para determinar os tipos dasfuncoes foldr e foldl.

7.2 Defina as funcoes length e map usando a funcao foldl.

7.3 Apresente duas definicoes da funcao reverse usando foldl e foldr.

7.4 Defina a funcao foldr usando a funcao foldl.

7.5 Considere a seguinte definicao:

sel f x y| (f x y) = x| otherwise = y

(1) Qual o tipo desta funcao?(2) Defina a funcao max que retorna o maximo entre dois numeros, usando a

funcao sel.

7.6 Relembre aquilo que dissemos na pagina 32: uma funcao f definida por recursivi-dade primitiva era da forma

f 0 = cf (n+1) = g n (f n)

Ou seja, precisamos de fornecer o valor que a funcao retorna quando o seu argu-mento e zero – o valor c – e a forma de obter o resultado de f (n+1) a partirdo valor de f n – a funcao g. Resumindo estas ideias, podemos apresentar aseguinte definicao.

primrec c g 0 = cprimrec c g (n+1) = g n (primrec c g n)

Usando esta definicao, a funcao f acima sera entao descrita como primrec c g.(1) Usando a funcao primrec reescreva a definicao da funcao factorial.(2) Escreva a definicao de uma funcao primrec’ que traduza a ideia de definicao

de funcoes recursivas sobre naturais usando acumuladores.

7.7 Usando as funcoes scanr e/ou scanl defina as seguintes funcoes(1) inits que constroi os segmentos iniciais de uma lista. Por exemplo inits

[1,2] = [[],[1],[1,2]].(2) tails que constroi os segmentos finais de uma lista. Por exemplo tails

[1,2] = [[1,2],[2],[]].

7.8 A funcao iterate funciona como um ciclo (infinito) de aplicacoes de uma funcao.Assim (se fn representar a aplicacao de f n vezes), iterate f x = [f0 x, f1

x, f2 x, ..., fn x, ...].

Page 86: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

82 7. TRAVESSIA DE LISTAS

iterate :: (a -> a) -> a -> [a]iterate f x = x : iterate f (f x)

Escreva uma outra definicao desta funcao usando scanl ou scanr.

7.9 Defina a funcao separa :: (a -> Bool) -> [a] -> ([a],[a]) que “parte”uma dada lista num par de listas de acordo com a verificacao ou nao do predicadopassado como argumento, por parte dos seus elementos. Por exemplo,

separa odd [1,2,3,4,5] = ([1,3,5],[2,4])

Note a diferenca entre esta funcao e a funcao span definida acima.Prelude> span odd [1,2,3,4,5]

([1],[2,3,4,5])

Prelude>

7.10 A funcao (de ordem superior) sortBy define-se como

isortBy p [] = []isortBy p [x] = [x]isortBy p (h:t) = ins p h (isortBy p t) whereins p x [] = [x]ins p x (a:b)| (p x a) = x:(a:b)| otherwise = a:(ins p x b)

(1) Qual e o tipo da funcao isortBy?(2) Defina uma funcao desc, de forma a que

isortBy desc [1..5] = [5,4,3,2,1](3) Defina uma funcao f, de forma a que

isortBy f [("aaa",10), ("bbb",5), ("ccc",8)] =[("bbb",5),("ccc",8),("aaa",10)]

Page 87: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

CAPITULO 8

Representacao de dados

A escrita e uso de programas de computador, bem como muita da nossa actividadeintelectual, tem como principal objectivo a resolucao de problemas reais. Isto pode serfeito de duas formas: pela accao directa sobre o mundo, ou entao usando um modeloVejamos um exemplo: pretendemos pavimentar o chao de uma sala com um determi-nado tipo de azulejos; para isso temos que saber se temos dinheiro para os comprar.A primeira estrategia de resolucao deste problema, por accao directa sobre o mundo,implicaria que fizessemos o seguinte acordo com o vendedor dos tais azulejos. Eleemprestar-nos-ia uma quantidade ilimitada de azulejos e nos coloca-los-ıamos. Quandoesta tarefa estivesse concluıda, poderıamos somar os custos dos azulejos utilizados everıamos se tınhamos ou nao o dinheiro suficiente para esta operacao.A outra estrategia, passa pela construcao de um modelo da parte do mundo que nosinteressa para resolver o problema. Este modelo deve ser o mais abstracto1 possıvel,de forma a reproduzir apenas a parte do mundo que e relevante para a resolucao doproblema em causa. Desta forma e mais facil agir (ou raciocinar) sobre ele.Um modelo possıvel para resolver o problema dos azulejos e a construcao de umaminiatura da sala em causa, bem como de miniaturas dos azulejos a colocar. Feitoisto devemos pavimentar a nossa miniatura com os tais azulejos e depois somar oscustos dos azulejos utilizados.Esta solucao pode nao parecer muito pratica, mas e, sem duvida alguma, um avancorelativamente a primeira estrategia.O passo de abstraccao dado pode ser ainda maior. Isto significa que podemos construirum modelo para resolver o problema que esqueca ainda mais pormenores. Por exemplopodemos determinar a area de cada azulejo, a area da sala a pavimentar e dividir estasduas quantidades. O numero resultante desta divisao corresponde ao numero mınimode azulejos a comprar.Note-se que ambas estas abordagens sao usadas por exemplo na arquitectura onde seconstroem modelos mais ou menos abstractos, desde as plantas as maquetes.Depois de construıdo o modelo, e muitas vezes necessario efectuar uma nova trans-formacao para que esse modelo possa ser usado. E isto porque, muitas das vezes,o modelo determinado nao tem uma correspondencia directa na linguagem de pro-gramacao em causa.

1abstrair, [Do lat. abstrahere.] v. tr. Separar, considerar apenas uma parte do todo.

83

Page 88: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

84 8. REPRESENTACAO DE DADOS

Nestes casos temos que definir um novo tipo, ou, como ja fizemos noutras situacoes,usar um tipo no qual seja possıvel representar o modelo em causa. A cada umadas solucoes de um problema chamamos uma implementacao. De todas solucoespossıveis, umas estao correctas e outras nao. Dentro das implementacoes correctasumas sao melhores do que outras.Podemos apresentar criterios objectivos que nos permitam pronunciar sobre a cor-reccao de uma dada implementacao. Quanto a qualidade dessas mesmas imple-mentacoes, os criterios sao, na maioria dos casos, criterios parciais ou subjectivos.O criterio que nos permite ajuizar sobre a correccao de uma dada implementacao e:

(1) todo o elemento do tipo a implementar deve ser representavel nessa imple-mentacao;

(2) dada uma implementacao, e possıvel determinar univocamente qual o ele-mento que essa implementacao representa.

O exemplo que vamos usar para introduzir alguns conceitos sobre abstraccao e repre-sentacao de dados e o problema da representacao de conjuntos (finitos). Este exemploe de alguma forma paradigmatico pois na maioria das linguagens de programacao naoexiste uma forma imediata de representar conjuntos.As solucoes que aqui serao apresentadas sao baseadas nas construcoes de tipos jaestudadas – listas e produtos cartesianos. Veremos que, na maioria destes casos,vamos precisar de impor restricoes (ou invariantes) na forma dos elementos.Esta estrategia ja foi usada noutras definicoes. Por exemplo, quando falamos da funcaohead dissemos que era uma funcao que dada uma lista nao vazia retornava o seuprimeiro elemento. No entanto, ao escrevermos o tipo de head, dissemos que

head :: [a] -> a

Ou seja, estamos implicitamente a usar o tipo [a] com uma restricao – listas naovazias. Outros casos em que usamos este processo foi quando definimos funcoes sobrenumeros naturais. Nesses casos usamos sempre o tipo Int com a restricao de osnumeros nao serem negativos.Esta forma de implementar os modelos necessarios pode ser levada ainda mais longe.Podemos prescindir do tipo Bool – em vez disso usar o tipo Int com a restricao de oselementos serem apenas 0 (para representar False) e 1 (para representar True).

Mas voltemos ao nosso problema da representacao de conjuntos. Como ja dissemos,existem criterios objectivos de correccao de uma dada implementacao. Para avaliarmosa qualidade de uma dada representacao convem definir quais as operacoes que devemser definidas sobre esse modelo. As operacoes que vamos definir sobre conjuntos sao:

• pertence :: a -> Conjunto a -> Bool• uniao :: Conjunto a -> Conjunto a -> Conjunto a• interseccao :: Conjunto a -> Conjunto a -> Conjunto a• vazio? :: Conjunto a -> Bool

Page 89: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

8. REPRESENTACAO DE DADOS 85

• escolha :: Conjunto a -> (a,Conjunto a)

Todas estas operacoes sao conhecidas, excepto, talvez, a ultima. Esta e uma operacaoparcial que, dado um conjunto nao vazio, da como resultado em elemento desseconjunto e o conjunto original sem esse elemento.A primeira escolha para representar conjuntos sao listas sem quaisquer restricoes. Umconjunto e representado por uma lista com os seus elementos. Esta representacaosatisfaz as duas clausulas acima.Para nos podermos pronunciar sobre a qualidade desta representacao vamos apresentardefinicoes das varias operacoes.

type Conjunto a = [a]

pertence x [] = Falsepertence x (a:aa) = (x == a) || (pertence x aa)

uniao l1 l2 = l1 ++l2

interseccao l1 l2 = [x | x <- l1, pertence x l2]

vazio? l = null l

escolha (x:xx) = (x, [y | y <- xx, y /= x])

Uma caracterıstica que deve ser realcada desta representacao e a sua simplicidade.Esta simplicidade tem, contudo, um preco – a ineficiencia. Esta e particularmenteevidente na escolha e interseccao de conjuntos.

• Uma vez que um mesmo elemento pode aparecer repetido na lista que rep-resenta o conjunto, a operacao de escolha tem de percorrer toda a lista.• Ao calcularmos a interseccao de dois conjuntos, a lista que representa o

segundo conjunto e percorrida tantas vezes quantos elementos existirem nalista que representa o primeiro conjunto.

Uma forma de melhorarmos a eficiencia da escolha e impormos a restricao de que naoguardamos elementos repetidos. Como ja dissemos, esta restricao nao se vai reflectir notipo Haskell que vamos escolher. Vai ter de ser uma propriedade que, assumindo quese verifica em todos os elementos deste tipo recebidos como argumento das operacoes,temos que garantir que se verifica nos resultados produzidos.Na definicao abaixo vamos optar por explicitar essa restricao como um comentario.Em Haskell ha duas formas de escrever comentarios:

• usando o sımbolo -- seguido do comentario. Neste caso e considerado co-mentario todo o texto ate ao fim da linha.

Page 90: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

86 8. REPRESENTACAO DE DADOS

• delimitando os comentarios pelos sımbolos {- e -}; desta forma podem-seincluir comentarios com varias linhas.

O uso da restricao tem como consequencia piorarmos a eficiencia da funcao de uniao deconjuntos. Deixamos de poder fazer apenas concatenacao de listas – tal nao garantiriaque a tal restricao se verificasse no resultado.

type Conjunto a = [a] -- sem elementos repetidos

pertence x [] = Falsepertence x (a:aa) = (x == a) || (pertence x aa)

uniao l1 l2 = l1 ++ [x | x <- l2, not (pertence x l1)]

interseccao l1 l2 = [x | x <- l1, pertence x l2]

vazio? l = null l

escolha (x:xx) = (x, xx)

Esta solucao continua a ter como (ma) caracterıstica o ter de percorrer varias vezesuma da listas na interseccao. De facto, nesta segunda versao, tambem a uniao sofredeste problema.Uma forma de contornar este problema e manter os elementos da lista ordenados(veremos mais adiante que isto nao e sempre possıvel).Alem de melhorar as eficiencias da uniao e interseccao de conjuntos, esta restricaovai-nos permitir ainda melhorar a eficiencia do teste de pertenca – mal encontremosum elemento maior do que aquele que procuramos podemos concluir que o elementonao pertence ao conjunto.

type Conjunto a = [a] -- ordenados por ordem crescente-- e sem repeticoes

pertence x [] = Falsepertence x (a:aa)| (x == a) = True| x > a = False| otherwise = pertence x aa

uniao [] l = luniao l [] = luniao (a:aa) (b:bb)| a < b = a:(uniao aa (b:bb))

Page 91: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

8. REPRESENTACAO DE DADOS 87

| a > b = b:(uniao (a:aa) bb)| otherwise = a:(uniao aa bb)

interseccao [] l = []interseccao l [] = []interseccao (a:aa) (b:bb)| a < b = interseccao aa (b:bb)| a > b = interseccao (a:aa) bb| otherwise = a:(interseccao aa bb)

vazio? l = null l

escolha (x:xx) = (x, xx)

Vejamos um outro exemplo. Vamos escrever definicoes de funcoes que operem sobrepolinomios (com uma unica variavel). Da mesma forma que fizemos atras com osconjuntos, devemos, antes de mais, decidir qual a forma de representar polinomiosem Haskell. Esta escolha esta quase sempre relacionada com as operacoes quepretendemos efectuar sobre o tipo a representar. No caso presente, vamos escreverdefinicoes das seguintes operacoes:

• adicao :: Polinomio -> Polinomio -> Polinomio• mutiplicacao :: Polinomio -> Polinomio -> Polinomio• valor :: Polinomio -> Double -> Double que calcula o valor de um

polinomio num ponto.

Vamos apresentar duas alternativas para representar polinomios. Para cada uma dasalternativas, vamos mostrar como seria representado o polinomio 3x5 − 5x4 + 4x2 −2x+ 10. Note-se que uma outra forma de escrever este polinomio e

3x5 + (−5)x4 + 0x3 + 4x2 + (−2)x1 + 10x0

• Podemos representar um polinomio como uma sequencia de pares – cadapar correspondera a um dos monomios e sera composto pelo coeficiente eexpoente do monomio. Nesta representacao, o polinomio acima seria repre-sentado por

[(10.0,0), (-2.0,1), (4.0,2), (-5.0,4), (3.0,5)]

A ordem pela qual os monomios aparece nao e relevante, pois para cada umdeles, guardamos tanto o coeficiente como o expoente. Outra caracterısticadesta representacao e que nos permite representar polinomios nao normaliza-dos, i.e., onde aparecem varios monomios com o mesmo grau. Por exemplo,o polinomio acima poderia ser representado por

[(10.0,0), (-2.0,1), (4.0,2), (-5.0,4), (-3.0,5), (6.0,5)]

Page 92: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

88 8. REPRESENTACAO DE DADOS

Aquilo que estamos a representar realmente e o polinomio 6x5−3x5−5x4 +4x2 − 2x+ 10 que sabemos ser equivalente ao polinomio original.• A segunda forma de representar polinomios corresponde a armazenar apenas

os coeficientes dos varios monomios. Nesta representacao, o polinomio acimaseria representado por

[10.0, -2.0, 4.0, 0.0, -5.0, 3.0]

Note-se que, nesta ultima representacao, devemos guardar a informacao sobreos coeficientes de todos os monomios – daı que aparecam mais elementosnesta ultima lista. Alem disso, devemos estipular uma ordem pela qual osvarios coeficientes aparecem. No exemplo acima, estes aparecem por ordemdecrescente do grau.

Alem disso, so podemos representar polinomios normalizados, i.e., comum monomio para cada um dos graus.

Vejamos agora como definir as varias operacoes sobre polinomios para a primeirarepresentacao.

type Polinomio = [(Int,Double)]

adicao p1 p2 = p1 ++ p2multiplicacao p1 p2 =[(e1*e2, c1+c2) | (e1,c1) <- p1, (e2,c2) <- p2]

valor p x = sum [c * (x ˆ e) | (e,c) <- p]

Esta solucao tem a evidente vantagem de ser simples. E contudo uma solucao muitopouco eficiente.Todas as operacoes sao tanto mais dispendiosas quanto mais longas forem as listasque representam os polinomios. Ora, ambas as operacoes que produzem polinomios(a adicao e a multiplicacao) nao fazem qualquer esforco por manter os seus resultadoscurtos.Uma forma de evitar estas desvantagens e a de considerar apenas polinomios nor-malizados, i.e., com apenas um monomio de cada grau. Para assegurarmos que estarestricao se verifica, sempre que adicionamos um novo elemento (monomio) a uma listatemos de ver se existe nessa lista um outro monomio com o mesmo grau. Se tal acon-tecer devemos apenas adicionar os coeficientes. Vamos basear a nossa implementacaonuma funcao – adMonomio – que adiciona um monomio a um polinomio.

type Polinomio = [(Int,Double)] -- normalizados

adicao p1 p2 = foldr adMonomio p1 p2multiplicacao p1 p2 = foldr adMonomio []

[(e1*e2, c1+c2) | (e1,c1) <- p1, (e2,c2) <- p2]valor p x = sum [c * (x ˆ e) | (e,c) <- p]

Page 93: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

8. REPRESENTACAO DE DADOS 89

adMonomio :: (Int,Double) -> Polinomio -> PolinomioadMonomio (e,c) [] = [(e,c)]adMonomio (e,c) ((a,b):resto)| e == a = (a,c+b):resto| otherwise = (a,b):(adMonomio (e,c) resto)

Esta ultima funcao podia ser melhorada se assumıssemos que os polinomios, alem deestarem normalizados, sao armazenados por ordem (crescente) do grau. Dessa forma,e tal como aconteceu acima na implementacao de conjuntos, nao temos que percorrertoda a lista quando nenhum monomio desse grau existe.

adMonomio :: (Int,Double) -> Polinomio -> PolinomioadMonomio (e,c) [] = [(e,c)]adMonomio (e,c) ((a,b):resto)| e == a = (a,c+b):resto| e < a = (e,c):(a,b):resto| e > a = (a,b):(adMonomio (e,c) resto)

Note-se contudo que com as restricoes que impusemos (polinomios normalizados earmazenados por ordem do grau) podemos definir a soma de polinomios de uma formamais eficiente:

adicao p1 [] = p1adicao [] p2 = p2adicao ((e1,c1):p1) ((e2,c2):p2)| e1 < e2 = (e1,c1):(adicao p1 ((e2,c2):p2)| e1 == e2 = (e1,c1+c2):(adicao p1 p2)| e1 > e2 = (e2,c2):(adicao ((e1,c1):p1) p2)

As sucessivas restricoes que fomos introduzindo a representacao inicial levam-nos a se-gunda representacao de polinomios. Alem de assumirmos que o polinomio se encontranormalizado e os monomios armazenados por ordem crescente de grau, vamos tambemassumir que os polinomios estao completos, no sentido que, para um polinomio de grauk guardamos todos os k + 1 coeficientes (mesmo os que sao nulos). Esta estrategiapermite-nos evitar o armazenamento dos expoentes dos varios monomios (este e de-duzido da posicao do coeficiente na lista – o primeiro corresponde ao de grau 0, osegundo ao de grau 1, e assim sucessivamente).Vejamos agora a implementacao das tres operacoes pretendidas.

Page 94: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

90 8. REPRESENTACAO DE DADOS

adicao [] q = qadicao p [] = padicao (p:ps) (q:qs) = (p+q):(adicao ps qs)

A estrutura desta funcao e muito semelhante a da funcao zipWith. A grande diferenca,e daı a razao para nao usar esta funcao, reside nos casos em que as duas listas temcomprimentos diferentes: enquanto que a funcao zipWith da como resultado umalista com comprimento igual ao menor das duas, a funcao que escrevemos acima fazexactamente o contrario – da como resultado uma lista com comprimento igual a damaior.Para definirmos a multiplicacao de polinomios para esta representacao, vamos comecarpor apresentar um exemplo de como tal operacao pode ser entendida:

2x2 +3x +4x2 +2x +3

6x2 +9x +124x3 +6x2 +8x

2x4 +3x3 +4x2

2x4 +7x3 +16x2 +17x +12Cada linha dos calculos intermedios corresponde a multiplicar um dos monomios dosegundo polinomio pelo primeiro. Esta multiplicacao faz-se multiplicando os coefi-cientes e arrastando para a esquerda o resultado tantas posicoes quanto o grau domonomio em causa. A multiplicacao de todos os coeficientes de um polinomio por umfactor pode ser feita usando a funcao map (ou de uma forma equivalente, usando listaspor compreensao)

multiplicacao [] p = []multiplicacao [x] p = [x * c | c <- p]multiplicacao (x:xs) p =adicao [x * c | c <- p] (0:(multiplicacao xs p))

Note-se que a segunda equacao nao e necessaria pois trata-se de um caso particularda terceira. Assim sendo, esta definicao tem a estrutura de um foldr e podemosescreve-la como tal.

multiplicacao p1 p2 = foldr (f p2) [] p1where f p x q = adicao [x * c | c <- p] (0:q)

Para completarmos esta implementacao temos apenas que definir a funcao que calculao valor de um polinomio num ponto. Uma forma de definir tal funcao e comecar porconverter os elementos desta implementacao na primeira de que falamos e depois usara funcao definida acima.

Page 95: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

8. REPRESENTACAO DE DADOS 91

valor p x =let p’ = zip [0..] pin sum [c * xˆe| (e,c) <- p’]

Esta estrategia tem no entanto um grande senao em termos de eficiencia. Uma vezque a potencia e calculada por sucessivas multiplicacoes, o calculo de por exemplo, x4

envolve o calculo de x3 e de x2. Ora estes calculos intermedios nao estao a ser usados,sendo por isso mesmo repetidos. Para resolver este problema vamos reorganizar ocalculo do valor de um polinomio num ponto. O polinomio

3x5 − 5x4 + 4x2 − 2x+ 10

pode ser reescrito como

3x5 + (−5)x4 + 0x3 + 4x2 + (−2)x1 + 10

Este, por sua vez, pode ser reescrito como

3 ∗ x5 + (−5) ∗ x4 + 0 ∗ x3 + 4 ∗ x2 + (−2) ∗ x+ 10(3 ∗ x4 + (−5) ∗ x3 + 0 ∗ x2 + 4 ∗ x+ (−2)) ∗ x+ 10((3 ∗ x3 + (−5) ∗ x2 + 0 ∗ x+ 4) ∗ x+ (−2)) ∗ x+ 10(((3 ∗ x2 + (−5) ∗ x+ 0) ∗ x+ 4) ∗ x+ (−2)) ∗ x+ 10((((3 ∗ x+ (−5)) ∗ x+ 0) ∗ x+ 4) ∗ x+ (−2)) ∗ x+ 10

(((((0 + 3) ∗ x+ (−5)) ∗ x+ 0) ∗ x+ 4) ∗ x+ (−2)) ∗ x+ 1010 + x ∗ ((−2) ∗ x ∗ (4 + x ∗ (0 + x ∗ ((−5 + x ∗ (3 + 0)))))

Esta formulacao do calculo evidencia a repeticao da operacao

c+ x ∗ rem que c corresponde aos coeficientes do polinomio.Podemos por isso exprimir este calculo usando a funcao foldr aplicada a lista 10,-2, 4, 0, -5, 3] (que e a representacao do dito polinomio nesta implementacao).Podemos entao escrever

valor x p = foldr f 0 pwhere f c r = c + x * r

Exercıcios

8.1 Nas representacoes de polinomios acima descritas (com excepcao da ultima),modifique as definicos das operacoes de adicao e multiplicacao de forma a eliminaros monomios com coeficiente nulo.

Page 96: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao
Page 97: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

CAPITULO 9

Tipos indutivos

Na introducao deste livro falamos de uma disciplina de tipos. Dissemos, entao, queexistiam duas especies de tipos: primitivos e compostos (ou estruturados).Nessa altura, descrevemos duas formas de estruturar tipos:

• produtos cartesianos• listas

Tomemos como exemplo a definicao

type Aluno = (Int , String)

Uma expressao deste tipo vai ser composta por duas partes: um numero inteiro e umastring. A forma de identificar cada uma destas partes faz-se a partir da sua posicao:a primeira componente diz respeito ao numero inteiro e a segunda a string.Uma outra forma de estruturar tipos de dados e atraves da definicao da soma ouco-produto de tipos. Na teoria de conjuntos, o co-produto e normalmente chamadouniao disjunta; dados dois conjuntos A e B, a sua uniao disjunta A+B e construıdada seguinte forma: por cada elemento de A existe um elemento em A+B com uma in-dicacao que esse elemento veio de A De igual forma, por cada elemento de B existe umelemento em A+B com uma indicacao que esse elemento veio de B. Adicionalmente,garante-se que estes sao os unicos elementos de A+B.Uma forma de representar o conjunto A+B e

A+B = {(1, a) | a ∈ A} ∪ {(2, b) | b ∈ B}

Os numeros 1 e 2 usados acima nao sao mais do que etiquetas que nos permitemdeterminar, para cada elemento de A+B, de onde e que esse elemento provem.Em Haskell, a definicao do co-produto de tipos e bastante simples; relativamentea construcao referida na teoria de conjuntos, a unica diferenca e que temos que sernos a fornecer as etiquetas que permitirao distinguir a origem dos elementos – estasetiquetas sao normalmente referidas como os construtores do tipo. Assim sendo,se tivermos os tipos A e B, para definirmos o tipo C como o co-produto destes dois,basta-nos fazer a seguinte declaracao:

data C = L1 A | L2 B

93

Page 98: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

94 9. TIPOS INDUTIVOS

As etiquetas L1 e L2 identificam a proveniencia de cada elemento do tipo C. Estasfuncoes podem ser usadas como quaisquer outras. Por exemplo, a funcao map L1 euma funcao que, dada uma lista de elementos do tipo A, retorna uma lista de elementosdo tipo C.Note-se ainda que, para definirmos o comportamento de uma funcao f : A+B → X,basta-nos definir o seu comportamento para os elementos de A e para os elementos deB; por outras palavras, logo que o comportamento de f esteja definido para todos oselementos de A (i.e., fica definida a funcao fA : A→ X) e para todos os elementos deB (i.e., fica definida a funcao fB : B → X), fica definido o comportamento de f . Ocomportamento desta funcao pode ser descrito por

f(x) =

fA(x) Se x vem de A

fB(x) Se x vem de B

Vejamos entao um exemplo de um tipo definido a partir do co-produto de outros dois:

data IntFloat = I Int | F Float

Elementos deste tipo tem duas formas (padroes) possıveis:

• I x em que x e uma expressao do tipo Int; por exemplo, I 3 ou I (3-2);• F y em que y e uma expressao do tipo Float; por exemplo F 3.4 ou F(2.4/12.0).

A forma de determinar a sua proveniencia e atraves da etiqueta que precede cadaelemento. Convem notarmos que, ao nıvel da sintaxe do Haskell, estas etiquetastem que comecar por uma letra maiuscula.Vejamos entao como estes tipos podem ser usados: Vamos definir a funcao convInt:: IntFloat -> Int que converte num inteiro um elemento deste tipo. Como dis-semos atras, para definir o comportamento desta funcao, basta-nos definir os seuscomportamentos parciais:

convInt (I x) = xconvInt (F x) = round x

Por outro lado, a funcao que selecciona de uma lista de elementos deste tipo, apenasaqueles que sao numeros inteiros, pode ser definida pela seguinte funcao:

inteiros :: [IntFloat] -> [IntFloat]inteiros [] = filter (eInt) l whereeInt (I x) = TrueeInt (F x) = False

Ao tentarmos experimentar esta definicao, deparamo-nos com uma situacao nova:

Page 99: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

9. TIPOS INDUTIVOS 95

Main> inteiros [I 3, F 3.4, I 39, F 0.0, F (-2.0)]

ERROR: Cannot find "show" function for:

*** expression : inteiros [I 3,F 3.4,I 39,F 0.0,F -2.0]

*** of type : [IntFloat]

Main>

Veremos mais a frente, no Capıtulo 11, a causa desta mensagem de erro; para ja vamosresolver o problema usando o comando :set -u:

Main> :set -u

Main> inteiros [I 3, F 3.4, I 39, F 0.0, F (-2.0)]

[I 3,I 39]

(44 reductions, 73 cells)

Main>

Embora sem o referirmos, ja deparamos com tipos cuja construcao assenta em co-produtos. Os exemplos mais simples deste tipo de construcao sao os tipos enumerados,como por exemplo o tipo Bool existente em Haskell. Uma possıvel definicao destetipo seria:

data Bool = False | True

Note-se que neste caso aparecem apenas as etiquetas.A funcao not:: Bool -> Bool define-se como:

not False = Truenot True = False

Uma outra aplicacao destes tipos enumerados seria a definicao de um tipo para rep-resentar os dias da semana:

data Semana= Domingo| Segunda| Terca| Quarta| Quinta| Sexta| Sabado

Uma funcao diaUtil:: Semana -> Bool que determina se um dado dia da semanae ou nao um dia util poderia ser definida como:

Page 100: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

96 9. TIPOS INDUTIVOS

diaUtil Segunda = TruediaUtil Terca = TruediaUtil Quarta = TruediaUtil Quinta = TruediaUtil Sexta = TruediaUtil Sabado = FalsediaUtil Domingo = False

Ou, de uma forma mais economica:

diaUtil Sabado = FalsediaUtil Domingo = FalsediaUtil _ = True

Suponhamos que armazenamos numa lista de pares os nomes e numeros de telefone.Considere-se agora uma funcao lookup que, dado um nome e uma dessas listas, calcula,caso exista, o numero de telefone associado a esse nome. Qual devera ser o tipo destafuncao? Note-se que em certas ocasioes a funcao devera dar um resultado equivalentea “numero desconhecido”; noutras deve dar como resultado um numero de telefone.Para resolver este problema podemos definir (de facto esta definido em Haskell) otipo

data Maybe a = Nothing | Just a

As duas alternativas correspondem as nossas duas possıveis ocasioes. Vejamos entaocomo definir a tal funcao lookup (que existe pre-definida).

lookup x [] = Nothing -- nao foi encontradolookup x ((a,b):resto)| x == a = Just b -- foi encontrado| otherwise = lookup x resto

Uma das principais utilizacoes de co-produtos em programacao funcional e a definicaode tipos recursivos. O exemplo paradigmatico de um tal tipo e o caso das listas.Uma possıvel definicao deste tipo e

data [a] = [] | a:[a]

Esta definicao diz-nos que as unicas formas de obter uma lista de elementos do tipo asao:

(1) a sequencia vazia,

Page 101: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

9. TIPOS INDUTIVOS 97

42

40 37

3820

35 49

60

85

70 100

Figura 1. Exemplo de uma arvore de inteiros

(2) a sequencia x:s, para um dado elemento x do tipo a e uma sequencia s deelementos do tipo a.

Uma generalizacao do conceito de sequencia e o de arvore. Por exemplo, umadefinicao de arvores binarias de inteiros seria:

data BTreeInt = Vazia | Nodo Int BTreeInt BTreeInt

Por exemplo a arvore apresentada na Figura 1 e representada pelo seguinte termo (dotipo ArvBinInt):

a = (Nodo 42 (Nodo 35 (Nodo 20 Vazia Vazia)(Nodo 38 (Nodo 37 Vazia Vazia)

(Nodo 40 Vazia Vazia)))(Nodo 49 Vazia

(Nodo 60 Vazia(Nodo 85 (Nodo 70 Vazia Vazia)

(Nodo 100 Vazia Vazia)))))

Vejamos agora algumas funcoes que manipulam objectos deste tipo.A funcao que soma todos os elementos de uma tal arvore (o equivalente a funcao sumdefinida para listas) define-se como:

somaArv :: BTreeInt -> IntsomaArv Vazia = 0

Page 102: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

98 9. TIPOS INDUTIVOS

somaArv (Nodo x esq dir)= x + (somaArv esq) + (somaArv dir)

A funcao seguinte retorna uma lista com os elementos de uma arvore binaria:

preorder :: BTreeInt -> [Int]preorder Vazia = []preorder (Nodo x esq dir)= [x] ++ (preorder esq) ++ (preorder dir)

Finalmente, a funcao inverteArv, semelhante a funcao reverse sobre listas, e que“inverte” uma arvore, pode ser definida como:

inverteArv :: BTreeInt -> BTreeIntinverteArv Vazia = VaziainverteArv (Nodo x esq dir)= Nodo x (inverteArv dir) (inverteArv esq)

Na definicao anterior, a escolha do tipo dos numeros inteiros para tipo dos elementosdas sequencias ou das arvores foi perfeitamente arbitraria. Com isto queremos dizerque, para definir o tipo das arvores binarias cujos elementos sao strings, procederıamosda mesma forma, substituindo o tipo Int da definicao pelo tipo String.Tal como na definicao das listas, e possıvel definir todos estes tipos de uma so vez,usando para isso variaveis de tipos.

data BTree t= Vazia| Nodo t (BTree t) (BTree t)

Podemos recuperar a definicao anterior de arvores binarias de inteiros, apenas com adefinicao da seguinte abreviatura

type BTreeInt = BTree Int

Vimos acima que, nas definicoes usando data, cada uma das alternativas deve incluirum construtor. Por exemplo, nas arvores binarias os construtores eram Vazia e Nodo.Dissemos ainda que estes construtores sao funcoes. Podemos verificar isso usando uminterpretador.

Main> :t Vazia

Vazia :: BTree a

Main> :t Nodo

Nodo :: a -> BTree a -> BTree a -> BTree a

Page 103: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

9. TIPOS INDUTIVOS 99

Um conceito relacionado e o de selector. Por exemplo, as arvores binarias nao vaziastem tres componentes; podemos por isso definir funcoes que seleccionem cada umadestas componentes. Uma forma de obter tal resultado e definindo funcoes que actuamsobre os tipos definidos.

raiz :: Btree a -> araiz (Nodo x _ _) = xesq, dir :: Btree a -> Btree aesq (Nodo _ e _) = edir (Nodo _ _ d) = d

Existe no entanto em Haskell uma alternativa que consiste em incorporar estasultimas definicoes na propria definicao do tipo. Por exemplo, para as arvores binariasacima podemos fazer a seguinte definicao.

data Btree t= Vazia| Nodo{ raiz :: t, esq :: Btree t, dir :: Btree t}

Para alem do mais, esta facilidade torna ainda disponıvel uma outra alternativa paraa definicao de elementos deste tipo. Por exemplo, a arvore da pagina 97 pode serdefinida em Haskell pela seguinte expressao.

a = Nodo{ raiz = 42, esq = Nodo { raiz = 35

, esq = Nodo 20 Vazia Vazia, dir = Nodo { raiz = 38

, esq = Nodo 37 Vazia Vazia, dir = Nodo 40 Vazia Vazia }}

, dir = Nodo { raiz = 49, esq = Vazia, dir = Nodo { raiz = 60

, esq = Vazia, dir = Nodo { raiz = 85

, esq = Nodo 70 Vazia Vazia, dir = Nodo 100 Vazia Vazia

}}}}

Page 104: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

100 9. TIPOS INDUTIVOS

4 100 42320

25

43 42

3

100

4

2

Figura 2. Exemplo de arvore irregular

Vejamos um outro exemplo de tipos parametrizados – as arvores irregulares, i.e., emque cada nodo pode ter um numero arbitrario de descendentes (na literatura anglo-saxonica, estas arvores sao conhecidas como Rose trees). Uma definicao possıvel paraeste tipo de dados e:

data RTree t = V | N {raiz :: t, desc ::[(RTree t)]}

Para representarmos a arvore da Figura 2, e usando os selectores definidos, temos aseguinte expressao.

N { raiz = 2, desc = [ N { raiz = 25

, desc = [ N { raiz = 43, desc [ N { raiz = 4, desc = []}

, N { raiz = 100, desc = []}]}, N { raiz = 2, desc = []}, N { raiz = 4, desc = []}]}

, N { raiz = 3, desc = []}, N { raiz = 4

, desc = [ N { raiz = 100, desc = [ N { raiz = 20, desc = []}

, N { raiz = 3, desc = []}, N { raiz = 2, desc = []}, N { raiz = 4, desc = []}]}]}]}

Sem usar os selectores, esta mesma expressao tem a seguinte forma.

Page 105: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

9. TIPOS INDUTIVOS 101

(N 2 [(N 25 [(N 43 [(N 4 []), (N 100 [])]),(N 2 []), (N 4 [])]),

(N 3 []),(N 4 [(N 100 [(N 20 []),(N 3 []), (N 2 []), (N 4 [])])])])

Vejamos como construir funcoes de manipulacao deste tipo de arvores. Por exemplo,para definirmos a funcao contanodos :: RTree t -> Int que retorna o numero denodos de uma arvore, procedemos da forma usual, i.e., por analise de casos:

• para uma arvore vazia, o numero de nodos e zero;• o numero de nodos de uma arvore da forma N r [a1...an] nao e mais do

que 1 mais o somatorio do numero de nodos de todas as sub-arvores a1. . . an.

A definicao vem por isso

contanodos V = 0contanodos (N r rs)= 1 + (sum (map contanodos rs))

Uma outra funcao com uma estrutura muito semelhante e a funcao que calcula o pesode uma arvore:

• o peso de uma arvore vazia e zero• o peso de uma arvore da forma N r [a1...an] nao e mais do que 1 mais o

maior dos pesos de todas as sub-arvores a1. . . an.

peso V = 0peso (N r []) = 1peso (N r (x:xs))= 1 + (maximum (map peso (x:xs)))

Os exemplos de definicoes recursivas acima sugerem que tambem para este tipo epossıvel definir uma funcao de ordem superior que resuma o esquema apresentado; talfuncao e, por isso mesmo, o equivalente as funcoes foldr e primrec definidas atras(paginas 75 e 81).

foldRTree f e V = efoldRTree f e (N r l) =let l’ = map (foldRTree f e) lin f e l’

Usando esta funcao podemos redefinir as funcoes contanodos e peso acima:

• contanodos a = foldRTree f 0 awhere f r l = 1 + (sum l)

Page 106: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

102 9. TIPOS INDUTIVOS

• peso a = foldRTree f 0 a wheref r [] = 1f r (x:xs) = 1 + (maximum l)

Exercıcios

9.1 Defina uma funcao semana_Int :: Semana -> Int que transforme cada dia dasemana num numero inteiro distinto (Sugestao: escreva a funcao de tal forma queDomingo seja transformado em 0, Segunda-feira em 1 e assim sucessivamente.

9.2 Defina uma funcao inversa da anterior; isto e, defina uma funcao int_Semanacom tipo :: Int -> Semana que satisfaca a propriedade

int Semana (semana Int s) = s

para todo o dia da semana s.

9.3 Porque e que e impossıvel definir uma funcao int_Semana que seja realmentea inversa de semana_Int, isto e que para alem da propriedade acima, verifiqueainda que

semana_Int (int_Semana n) = n

para todo o numero inteiro n?

9.4 Usando as funcoes anteriores, defina funcoes amanha e ontem que, dados um diada semana, retornem qual o dia da semana que sera no dia seguinte e no diaanterior.

9.5 Defina uma funcao que determine o numero de elementos de uma arvore.

9.6 Redefina a funcao anterior usando as funcoes preorder e length.

9.7 Defina uma funcao que determine o peso (i.e., nıvel maximo) de uma arvorebinaria (por exemplo, o peso da arvore a referida atras e 5).

9.8 Seja a a arvore binaria representada na Figura 1. Qual o resultado de preordera?

9.9 Defina uma funcao inorder :: BTreeInt -> [Int] que, tal como a funcaopreorder, devolva uma lista com todos os elementos da arvore; neste caso porem,verifica-se que

inorder a = [20,35,37,38,40,42,49,60,70,85,100]

9.10 Defina a funcao nodos :: RTree t -> [t] que retorna a lista dos elementosde uma arvore.

9.11 Defina a funcao maximo :: RTree t -> t que retorna o maior elemento deuma arvore (nao vazia).

9.12 Defina a funcao mapArv :: (a -> b) -> (RTree a) -> (RTree b) que aplicauma dada funcao a todos os elementos de uma arvore.

9.13 Qual o tipo da funcao foldArv?

Page 107: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

9. TIPOS INDUTIVOS 103

9.14 Redefina a funcao mapArv usando a funcao foldRTree.

9.15 Defina uma funcao trav :: BTree a -> [a] de forma que a funcao reconstroiabaixo satisfaca a propriedade

reconstroi (trav a) = a

Page 108: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao
Page 109: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

CAPITULO 10

Classes e polimorfismo

Considere-se a seguinte sequencia de calculos em hugs:Prelude> :set +t

Prelude> 3 + 4

7 :: Int

Prelude> 3.0 + 4.0

7.0 :: Double

Na primeira linha usamos o comando do hugs que nos permite saber o tipo da ex-pressao calculada. Os dois calculos seguintes dao-nos algumas indicacoes sobre o tipodo operador +:

• no primeiro caso fornecemos-lhe dois numeros inteiros e o resultado foi umnumero inteiro. A conclusao a tirar disto e que o tipo deste operador e

+ :: Int -> Int -> Int

• no segundo caso fornecemos-lhe dois numeros decimais e o resultado foi umnumero decimal. A conclusao a tirar disto e que o tipo deste operador e

+ :: Double -> Double -> Double

Qual sera afinal o tipo do operador +?

Considere-se agora a seguinte funcao sobre numeros inteiros

minmax :: Int -> Int -> (Int,Int)minmax x y| (x <= y) = (x,y)| otherwise = (y,x)

Como devemos fazer para que escrever uma versao desta funcao sobre numeros deci-mais?

Ambos os casos estao relacionados com uma propriedade que muitas linguagens deprogramacao permitem – que um mesmo sımbolo possa ser usado para operacoesdiferentes. A esta caracterıstica e comum chamar polimorfismo.Ja falamos de polimorfismo ao longo deste texto. Demos como exemplo paradigmatico,a definicao da funcao identidade:

105

Page 110: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

106 10. CLASSES E POLIMORFISMO

id :: a -> aid x = x

Podemos considerar que esta definicao corresponde a definir, para cada tipo X, umafuncao

idX :: X -> X

O polimorfismo e conseguido neste caso porque a definicao do comportamento dafuncao identidade e independente da forma dos seus argumentos. O mesmo nao sepassa nos dois exemplos apresentados acima – a adicao de numeros inteiros e umprocesso estruturalmente diferente da adicao de numeros decimais. Nao podemos porisso resolver estes casos usando este tipo de polimorfismo (normalmente conhecido porparametrico).Em Haskell existe uma outra forma de polimorfismo – o uso de classes.Continuemos a analisar o primeiro exemplo dado. Uma forma de resolver o conflitode tipos acima, usando para isso a parametrizacao, seria a de considerar que o tipoda funcao + e

(+) :: a -> a -> a

Se tal estivesse correcto, poderıamos calcular ’a’ + ’b’, obtendo um outro caracter!Ora isto nao faz qualquer sentido. Podemos por isso pensar que, apenas podem sersomados elementos de alguns tipos. Como isto e conseguido em Haskell e definindoa classe dos tipos cujos elementos podem ser somados. Esta classe chama-se Num.Vejamos entao o que acontece ao tentarmos reduzir a expressao ’a’ + ’b’:

Prelude> ’a’ + ’b’

ERROR: Char is not an instance of class "Num"

Prelude>

Uma forma de compreender o que sao classes e considera-las como tipos de tipos; desteponto de vista podemos dizer que o tipo Int e do tipo Num, enquanto que Char naoe. Esta relacao entre tipos e classes chama-se relacao de instancia – dizemos que Inte uma instancia Num.Vejamos um pequeno exemplo de definicao e uso de classes. Consideremos em primeirolugar as seguintes definicoes de tipos para representar figuras geometricas:

data Ponto = P Double Double -- P abcissa ordenadadata Rectangulo = Rect Ponto Double Doubledata Triangulo = Tri Ponto Ponto Ponto

Para qualquer destes tipos vamos precisar de definir uma funcao que faca uma translacao:

transPonto :: Double -> Double -> Ponto -> PontotransPonto deltaX deltaY (P x y) = P (x+deltaX) (y+deltaY)

Page 111: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

10. CLASSES E POLIMORFISMO 107

transRect :: Double -> Double -> Rectangulo -> RectangulotransRect dX dY (Rect p a l) = Rect (transPonto dX dY p) a l

transTri :: Double -> Double -> Triangulo -> TriangulotransTri dX dY (Tri a b c) =Tri (transPonto dX dY a)

(transPonto dX dY b)(transPonto dX dY c)

Uma forma alternativa de definir as funcoes acima consiste em definir a classe de tipospara os quais existe uma funcao de translacao:

class Trans t wheretranslacao :: Double -> Double -> t -> t

Esta definicao deve ser lida como

Para que um tipo t seja da classe Trans e necessario existir umafuncao translacao :: Double -> Double -> t -> t

Esta leitura explica a seguinte resposta:

Main> :t translacao

translacao :: Trans a => Double -> Double -> a -> a

Main>

O tipo da funcao translacao e composto por duas partes (separadas pelo sımbolo=>):

• do lado direito aparece o tipo propriamente dito, isto e, o tipo dos argumentosda funcao e o tipo do seu resultado.• do lado esquerdo aparecem restricoes; neste caso que o tipo a deve ser da

classe Trans.

Para que possamos usar a classe Trans, devemos definir as suas varias instancias. Porexemplo, para o tipo Ponto:

instance Trans Pont wheretranslacao deltaX deltaY (P x y) = P (x+deltaX) (y+deltaY)

A definicao da classe apenas dizia quais as funcoes que devem existir. A definicao deuma instancia corresponde a fornecer uma funcao para implementar cada uma dasfuncoes referidas na definicao da classe.Vejamos agora como definir os tipos Rectangulo e Triangulo como instancias daclasse Trans:

Page 112: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

108 10. CLASSES E POLIMORFISMO

instance Trans Rectangulo wheretranslacao dX dY (Rect p a l)= Rect (translacao dX dY p) a l

E importante notar que, apesar de, nesta definicao da funcao translacao, o sımbolotranslacao aparecer tambem do lado direito da equacao, esta nao e uma definicaorecursiva! Isto porque a funcao translacao que aparece no lado direito da equacaorefere-se a translacao de um ponto, e nao a translacao que esta a ser definida (derectangulos). Este tipo de polimorfismo – em que um mesmo sımbolo representaconceitos diferentes – e conhecido na literatura anglo-saxonica como overloading.Na definicao de Triangulo como instancia de Trans vamos usar a mesma estrategia:

instance Trans Triangulo wheretranslacao dX dY (Tri a b c) =Tri (translacao dX dY a)

(translacao dX dY b)(translacao dX dY c)

A definicao de uma classe pode ter, para alem da declaracao do tipo das funcoesdisponıveis para instancias dessa classe, a definicao de algumas dessas funcoes. Porexemplo, na classe Trans acima poderıamos ter definido funcoes de translacao numdos eixos:

class Trans t wheretranslacao :: Double -> Double -> t -> ttransX :: Double -> t -> ttransY :: Double -> t -> t

transX dX f = translacao dX 0.0 ftransY dY f = translacao 0.0 dX f

Desta forma, ao definirmos uma instancia da classe Trans, so e necessario fornecer adefinicao da funcao translacao – as funcoes transX e transY ficam automaticamentedefinidas para essa instancia em particular. Daı que as definicoes das instancias acimanao tenham que ser alteradas para esta nova definicao.O tipo destas funcoes e:

Main> :t transX

transX :: Trans a => Double -> a -> a

Main> :t transY

transX :: Trans a => Double -> a -> a

Podemos ate definir ainda a funcao de translacao a partir das definicoes de translacaosegundo um dos eixos.

Page 113: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

10. CLASSES E POLIMORFISMO 109

class Trans t wheretranslacao :: Double -> Double -> t -> ttransX :: Double -> t -> ttransY :: Double -> t -> t

transX dX f = translacao dX 0.0 ftransY dY f = translacao 0.0 dX f

translacao dx dy f = transX dx (transY dy f)

Desta forma, ao definir uma nova instancia deste tipo, podemos escolher entre fornecera definicao da funcao de translacao ou das translacoes segundo os eixos.Uma outra possibilidade na definicao de classes e instancias e a utilizacao de restricoes(tal como referido acima sobre os tipos das funcoes translacao, trasX e transY).Para compreender este mecanismo vamos declarar como instancias da classe Trans aslistas cujos elementos sao, eles proprios, instancias dessa classe.

instance (Trans f) => Trans [f]where translacao dX dY l = map (translacao dX dY) l

A primeira linha desta definicao deve ser lida comolistas de elementos do tipo f sao uma instancia da classe Trans

desde que esse tipo f seja tambem uma instancia da tal classe.Note-se ainda que, a funcao translacao que e usada para definir a translacao de listas(de elementos do tipo t) e a funcao correspondente ao tipo t. Daı que a definicaoacima nao seja uma definicao recursiva.

Para finalizar esta introducao ao uso de classes vamos apresentar duas das mais uti-lizadas classes pre-definidas em Haskell: Eq e Ord.

A classe Eq corresponde a classe dos tipos para os quais existe uma operacao deigualdade.

class Eq a where(==), (/=) :: a -> a -> Boolx /= y = not (x == y)

Note-se que como nesta definicao ja esta definida a funcao /=, as instancias destaclasse podem definir apenas a funcao ==.Para vermos quais os tipos que sao instancias da classe Eq podemos usar o comando:info do hugs:

Prelude> :info Eq

-- type class

Page 114: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

110 10. CLASSES E POLIMORFISMO

class Eq a where

(==) :: a -> a -> Bool

(/=) :: a -> a -> Bool

infix 4 ==

infix 4 /=

-- instances:

instance (Eq a, Eq b) => Eq (a,b)

instance (Eq a, Eq b, Eq c) => Eq (a,b,c)

instance (Eq a, Eq b, Eq c, Eq d) => Eq (a,b,c,d)

instance (Eq a, Eq b, Eq c, Eq d, Eq e) => Eq (a,b,c,d,e)

instance Integral a => Eq (Ratio a)

instance Eq Ordering

instance (Eq b, Eq a) => Eq (Either a b)

instance Eq a => Eq (Maybe a)

instance Eq Bool

instance Eq ()

instance Eq Char

instance Eq a => Eq [a]

instance Eq Int

instance Eq Integer

instance Eq Float

instance Eq Double

Prelude>

A classe Ord e a classe dos tipos para os quais existe uma funcao de comparacaoentre os seus elementos. A definicao desta classe assenta na existencia de um tipo(enumerado) Ordering cuja definicao e:

data Ordering = EQ | LT | LE | GT | GE

O significado destas varias alternativas e

• EQ (Equal) – igual• GT (Greater) – maior• GE (Greater or Equal) – maior ou igual• LT (Lower) – menor• LE (Lower or Equal) – menor ou igual

Na definicao que se segue existe alguma redundancia – as varias operacoes de com-paracao (<=, >,. . . ) estao definidas a custa da funcao compare enquanto que esta estadefinida a partir das primeiras.Esta circularidade tem, no entanto, um proposito muito util – ao definirmos uma novainstancia desta classe podemos optar por uma destas formas – a outra fica automati-camente definida.

Page 115: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

10. CLASSES E POLIMORFISMO 111

class (Eq a) => Ord a wherecompare :: a -> a -> Ordering(<), (<=), (>=), (>) :: a -> a -> Boolmax, min :: a -> a -> a

compare x y| x == y = EQ| x <= y = LT| otherwise = GTx <= y = compare x y /= GTx < y = compare x y == LTx >= y = compare x y /= LTx > y = compare x y == GTmax x y| x >= y = x| otherwise = ymin x y| x < y = x| otherwise = y

Na informacao que e fornecida pelo ghci sobre as classes podemos ver quais dasfuncoes estao definidas a custa de outras.

Prelude> :i Ord

-- Ord is a class

class (Eq a) => Ord a where {

compare :: a -> a -> Ordering {- has default method -};

(<=) :: a -> a -> Bool {- has default method -};

(>) :: a -> a -> Bool {- has default method -};

(>=) :: a -> a -> Bool {- has default method -};

(<) :: a -> a -> Bool {- has default method -};

min :: a -> a -> a {- has default method -};

max :: a -> a -> a {- has default method -};

}

Prelude>

Um outro conceito que esta patente na definicao da classe Ord e o de sub-classe. Aprimeira linha desta definicao deve ser lida como:

para que um tipo a seja da classe Ord e necessario que seja da classeEq e que estejam disponıveis as seguintes funcoes...

Assim sendo, todas as instancias do tipo Ord sao tambem instancias da classe Eq. Daıdizermos que a classe Ord e uma sub-classe de Eq.

Page 116: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

112 10. CLASSES E POLIMORFISMO

Ao definirmos novos tipos muitas vezes queremos que esses tipos sejam instanciasdestas classes. Normalmente estas instancias sao definidas de uma forma estrutural.Vejamos um exemplo: as arvores binarias defindas atras.

data BTree a = Vazia | Nodo t (BTree a) (BTree a)

instance (Eq a) => Eq (BTree a) whereVazia == Vazia = True(Nodo r1 e1 d1) == (Nodo r2 e2 d2)= r1 == r2 && e1 == e2 && d1 == d2_ == _ = False

Em Haskell existe um metodo para obter estas instancias (que sao baseadas naestrutura do tipo) de uma forma mais expedita. Esse mecanismo e conhecido porderivacao de instancias e esta disponıvel para um conjunto fixo de classes: Eq, Ord,Enum, Bounded, Show e Read.Por exemplo, a definicao das arvores binarias como instancia (derivada) das classes Eqe Ord pode ser feita aquando da definicao do tipo.

data BTree a = Vazia | Nodo t (BTree a) (BTree a)deriving (Eq,Ord)

Vamos terminar esta introducao ao uso de classes em Haskell com a definicao daclasse Functor. Como veremos, esta classe e diferente das que apresentamos acimapois trata-se de uma classe de construtores de tipos.Um dos exemplos de funcoes de ordem superior que falamos atras foi a funcao map queaplica uma funcao a todos os elementos de uma lista. Mais a frente mencionamos queesta funcao poderia ser tambem escrita para outras estruturas de dados. Referimos emparticular o caso das arvores e como se pode escrever uma funcao (de ordem superior)que aplique uma mesma funcao a todos os elementos de uma arvore.A constatacao desta generalidade da funcao map faz com que tentemos definir umaversao polimorfica da funcao map. Existe no entanto um problema com qual o tipoque esta funcao deve ter. Reparemos por exemplo nos tipos destas duas instancias deuma tal funcao polimorfica:

• (a ->b) -> [a] -> [b] para as listas e• (a ->b) -> BTree a -> BTree b para as arvores binarias.

Aquilo que muda nestes tipos e o construtor de tipos: no primeiro caso listas de eno segundo arvores de.A forma existente em Haskell e definir classes em que os elementos (i.e., as instancias)sao, nao tipos, mas construtores de tipos.A definicao da classe Functor existente em Haskell e

Page 117: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

10. CLASSES E POLIMORFISMO 113

class Functor f wherefmap :: (a -> b) -> f a -> f b

Uma possıvel definicao de arvores binarias como instancias desta classe e

instance Functor BTree where-- fmap :: (a -> b) -> BTree a -> BTree bfmap f Vazia = Vaziafmap f (Nodo a e d) = Nodo (f a) (fmap f e) (fmap f d)

Uma das aplicacoes mais frequentes do uso de classes e na modelacao de dados. Vimosno Capıtulo 8 que, ao escolhermos uma dada representacao para um modelo, e conve-niente saber quais as operacoes que devem estar disponıveis. Isso nao e mais do quea definicao de uma classe. A definicao de cada uma das representacoes corresponde,nesta prespectiva, a definicao de instancias dessa classes.Revendo os exercıcios que foram apresentados nessa altura, podemos definir as seguintesclasses:

class Conjunto c wherepertence :: (Eq a) => a -> c a -> Booluniao :: (Eq a) => c a -> c a -> c ainterseccao :: (Eq a) => c a -> c a -> c avazio? :: c a -> Bool}escolha :: c a -> Maybe (a,c a)

class Polinomio whereadicao :: Polinomio -> Polinomio -> Polinomiomutiplicacao :: Polinomio -> Polinomio -> Polinomiovalor :: Polinomio -> Double -> Double

Exercıcios

10.1 A classe Zoom pretende ser a classe das figuras geometricas que podem ser es-calaveis:

class Zoom t wherescale :: Double -> t -> t

Complete as seguintes definicoes:(1) instance Zoom Rectangulo where ...(2) instance Zoom Triangulo where ...(3) instance (Zoom t) => Zoom [t] where ...

Page 118: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

114 10. CLASSES E POLIMORFISMO

(4) instance (Zoom t,Zoom s) => Zoom (t,s) where ...

10.2 Considere agora o tipo Poligonal definido para representar linhas poligonais:

data Poligonal = Poly [Ponto]

Defina o tipo Poligonal como uma instancia da classe Trans.

10.3 A classe Conjunto pretende ser a classe (de construtores) que traduz a nocao deconjunto.

class Conjunto c whereelemento :: (Eq a) => a -> c a -> Boolescolha :: c a -> Maybe (a, c a)singular :: a -> c aacrescenta (Eq a) => a -> c a -> c a

(1) A funcao que calcula a uniao de dois conjuntos pode ser definida por

uniao:: (Eq a, Conjunto s) => s a -> s a -> s auniao c1 c2 =case escolha c1 ofNothing -> c2Just (e,c) -> acrescenta e (uniao c c2)

Defina a funcao interseccao :: (Eq a, Conjunto s) => s a -> sa -> s a.

(2) Defina listas como uma instancia desta classe, i.e., complete a seguintedefinicao

instance Conjunto [] where...

Por uma questao de eficiencia da implementacao, assuma que as listas estaoordenadas e sem elementos repetidos.

(3) Para representar multiconjuntos, i.e., conjuntos onde cada elemento do con-juto tem a si associada o numero de vezes que esse elemento ocorre, usou-sea seguinte definicao

data (Eq a) => Multiset a = M [(a,Int)]

Defina Multiset como uma instancia de Conjunto.(4) Complete a seguinte definicao

instance Conjunto s => Functor s where...

Page 119: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

CAPITULO 11

Interaccao

Neste capıtulo vamos apresentar um conceito que e muito importante na escrita deprogramas – a interaccao com o utilizador. Ate aqui apenas nos preocupamos emescrever funcoes que recebiam argumentos de um dado tipo e produziam resultadosde um outro tipo. No entanto toda a interaccao com o utilizador de tais funcoes sefaz atraves de sequencias de carateres.Vejamos o que acontece associado ao seguinte excerto do funcionamento do interpre-tador:

Prelude> (sqrt 64) ˆ 2

64.0

Prelude>

(1) e lida uma sequencia de carateres ("(sqrt 64) ^ 2") que e convertida parauma expressao (equivalente a (

√64)2);

(2) o valor da expressao e calculado (por sucessivas reducoes) obtendo o seu valor– o numero 64;

(3) e calculada a string que representa este numero ("64.0") e escrita no ecran.

Esta sequencia de accoes e normalmente designada por read, eval, and print.Estreitamente relacionadas com este ciclo de funcionamento, existem em Haskellduas classes: Read e Show.

• Para que um tipo pertenca a classe Read e necessario existir uma funcao –read – que transforme Strings em elementos desse tipo:

Prelude> (read "12345") :: Int

12345

Prelude> (read "[123,345]") :: [Int]

[123,345]

Prelude> (read "123 abc") :: Int

*** Exception: Prelude.read: no parse

Prelude> (read "(123, 234)") :: (Int,Float)

(123,234.0)

• Para que um tipo pertenca a classe Show e necessario existir uma funcao –show – que transforme elementos desse tipo em Strings:

Prelude> show 12345

"12345"

Prelude> show [123,354]

115

Page 120: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

116 11. INTERACCAO

"[123,354]"

Prelude> show (123,324.0)

"(123,324.0)"

Esta descricao das classes Read e Show e talvez simplista demais. Veremos mais afrente uma descricao mais promenorizada destas classes.Uma caracterıstica importante de ambas as classes e que elas admitem instanciasderivadas. Assim, ao definirmos um novo tipo, podemos usar essa caracterıstica parao definirmos como instancia destas classes.Vejamos por exemplo as arvores binarias; ao fazermos a declaracao

module ArvoresBinarias wheredata ArvB a = V | Nodo a (ArvB a) (ArvB a)deriving (Read, Show)

depth :: ArvB a -> Intdepth V = 0depth (Nodo _ e d) = 1 + (max (depth e) (depth d))

podemos converter arvores em strings e vice versa.

ArvoresBinarias> (read "Nodo 2 V V") :: ArvB Int

Nodo 2 V V

*ArvoresBinarias> depth ((read "Nodo 2 (Nodo 3 V V) V")::ArvB Int)

2

*ArvoresBinarias> show (Nodo 2 V V)

"Nodo 2 V V"

Os programas que temos vindo a apresentar sao funcoes. Como tal, a execucao de umprograma e feita atraves de sucessivas reducoes. Para que este processo de reducaoseja iniciado temos que invocar a funcao pretendida com os argumentos apropriados.Ha no entanto um conjunto importante de programas em que pretendemos que outilizador desses programas va introduzindo os dados de uma forma incremental. Poroutras palavras, pretendemos muitas vezes escrever programas (i.e., funcoes) cujosargumentos vao sendo fornecidos de uma forma gradual.Podemos ainda ver este problema de uma outra forma. Os programas que temosvindo a escrever sao programas que apenas comunicam com o utilizador num sentido– fornecendo os resultados dos seus calculos.Aquilo que distingue os programas interactivos dos outros (muitas vezes chamadosautistas) e que nestes ultimos, a sequencia de accoes a executar e, invariavelmente

Leitura dos dados → Calculo → Escrita dos resultados

Nos programas interactivos, nao existe uma divisao estanque entre estas tres fases.

Page 121: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

11. INTERACCAO 117

A forma de obter tal efeito num programa funcional e atraves da “composicao”, oumelhor dito, da sequenciacao, de programas que verifiquem o esquema acima.Em Haskell a abordagem adoptada para resolver este problema e baseada no uso deum formalismo conhecido por monades. Num curso de introducao a programacao,o estudo deste formalismo pode parecer precoce. Vamos por isso, e para ja, tentarabordar tal conceito apenas na perspectiva da interaccao.

O uso de monades esta associado a tentativa de formalizacao da nocao de com-putacao. Uma computacao pode ser vista como uma “caixa negra”. A execucaode uma computacao produz um determinado resultado, eventualmente modificando oseu estado interno. Isto e conceptualmente diferente do conceito de funcao. Nestasultimas a nocao de estado interno nao existe – ao executarmos duas vezes a mesmafuncao com os mesmos argumentos, obtemos duas vezes o mesmo resultado.Baseado neste conceito de computacao, define-se programa como uma funcao querecebe como argumento um valor e retorna como resultado uma computacao.Graficamente, um programa pode ser visto como uma “caixa negra” com uma entrada(o argumento da funcao) e uma saıda (o resultado da computacao).Em Haskell existe definido o tipo IO a que corresponde a uma computacao que(fazendo eventualmente operacoes de input/output) calcula um valor do tipo a.As operacoes fundamentais que se podem definir sobre este tipo (e alias, sobre qualquermonade) sao:

• return :: a -> IO a que corresponde ao programa que, sem modificar oseu estado interno apenas da como resultado o valor da sua entrada. Esteprograma corresponde a uma generalizacao da funcao identidade.• >>= :: IO a -> (a -> IO b) -> IO b que corresponde a sequenciacao

de computacoes. Dada uma computacao c e um programa p, o termo c>>=p corresponde a seguinte computacao:(1) comeca-se por executar a computacao c; esta produz um resultado x

(do tipo a);(2) de seguida executa-se o programa p fornecendo como entrada esse valor

x, i.e., executa-se a computacao p x.• >> :: IO a -> IO b -> IO b que corresponde a uma variante da operacao

anterior. Neste caso o valor calculado pela primeira computacao e ignorado(i.e., nao e passado como parametro a segunda computacao).

Estas operacoes podem ser vistas como combinadores de computacoes, permitindo, apartir de computacoes simples, construir computacoes mais complexas.Para as podermos usar, devemos ter a nossa disposicao um conjunto de computacoese programas elementares. Quanto a interaccao propriamente dita, essas componentesatomicas sao:

• putChar:: Char -> IO () que corresponde ao programa que escreve umcaracter no ecran e nao retorna qualquer valor;

Page 122: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

118 11. INTERACCAO

• getChar:: IO Char que corresponde a computacao que le um caracter (doteclado) e da como resultado o caracter lido.

Usando estas duas operacoes podemos definir outras mais complexas.Vejamos em primeiro lugar um programa que le um caracter e, caso ele seja uma letraminuscula escreve a correspondente letra maiuscula.

minuscula :: IO ()minuscula = getChar >>= continua

Atente-se em primeiro lugar no tipo deste programa: trata-se de uma computacao quenao retorna qualquer valor.A primeira accao e ler um caracter. Esta computacao calcula um valor que e fornecidoao programa continua. para isso e necessario que este seja do tipo Char -> IO ().

continua x =if (minuscula x)then putChar (convMaiuscula x)else return ()

A funcao minuscula testa se um dado caracter e uma letra minuscula. A funcaoconvMaiuscula converte uma letra minuscula na correspondente maiuscula.Mas vejamos com mais detalhe esta ultima funcao. Usando a notacao-λ, poderıamoster escrito:

continua =(\x ->if (minuscula x)then putChar (convMaiuscula x)else return ())

E por isso mesmo poderıamos ter escrito a computacao inicial como:

minuscula :: IO ()minuscula =getChar >>= (\x ->if (minuscula x)then putChar (convMaiuscula x)else return ())

Escrito desta forma (e com esta disposicao do texto) podemos pensar que o valor quee calculado pela computacao getChar e temporariamente armazenado com o nome x.

Page 123: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

11. INTERACCAO 119

Este genero de definicoes e muito habitual na escrita de programas interactivos. Poresta razao existe em Haskell uma notacao propria para estas situacoes. Assim, emvez de escrevermos

prog1 >>= (\x1 ->prog2 >>= (\x2 ->...progn >>= (\xn ->prog)...))

Vamos usar a sintaxedo { x1 <- prog1

; x2 <- prog2; ...; xn <- progn; prog}

Esta sintaxe e normalmente conhecida por notacao do. O exemplo acima, escritonesta notacao, fica:

minuscula :: IO ()minuscula =do{ x <- getChar; if (minuscula x)then putChar (convMaiuscula x)else return ()

}

Vejamos agora outros exemplos simples de utilizacao dos combinadores acima. Porexemplo, um programa que escreva uma dada string:

putStr [] = return ()putStr (x:xs) = putChar x >> putStr xs

A correspondente operacao de leitura tambem e facil de exprimir, usando apenasgetChar; o unico cuidado a ter e saber quando terminar a leitura. Neste caso, vamosler caracteres ate que apareca o caracter de fim de linha (’\n’)

getLine =do{ x <- getChar; if x == ’\n’ then return []else do

Page 124: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

120 11. INTERACCAO

{ l <- getLine; return (x:l)}

}

Note-se que estas operacoes existem pre-definidas em Haskell. Com este programapodemos definir um programa para ler uma linha apos ter escrito uma mensagem:

dialogo out = do{ putStr out; getLine}

O tipo deste programa e String -> IO String. Podemos generalizar este programapara efectuar uma serie de interaccoes:

mdialogo :: [String] -> IO [String]mdialogo [] = return []mdialogo (frase:frases) = do{ resposta <- dialogo frase; respostas <- mdialogo frases; return (resposta:respostas)}

Para alem das operacoes de interaccao ja apresentadas, existem pre-definidas emHaskell as seguintes:

• getContents :: IO String que obtem todo o input de uma so vez. Comoveremos, esta funcao usada num ambiente de lazy evaluation pode ser usadapara escrever programas interactivos.• interact :: (String -> String) -> IO () que aplica uma dada funcao

ao input para obter o output.

Estas ultima operacao pode ser definda a custa da primera:

interact f = do{ input <- getContents; putStr (f input)}

E de notar que nas versoes actuais do hugs e do ghci, estas duas operacoes tem umcomportamento diferente. Enquanto que em ghci o input tambem aparece no ecran,tal nao acontece no hugs. Veja-se esta diferenca quando fornecemos como input o textoaaaa\nbb\nc (em que \n representa mudanca de linha). Usando o interpretador ghciobtemos o seguinte resultado

Page 125: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

11. INTERACCAO 121

Prelude> getContents >>= (putStr . show . (map length) . lines)

a[aaa

4b,b

2c,1]Prelude>

enquanto que em hugs o resultado ePrelude> getContents >>= (putStr . show . (map length) . lines)

[4,2,1]

Prelude>

As definicoes que escrevemos ate aqui destinavam-se exclusivamente a ser executadasa partir do interpretador (ghci ou hugs). Existe no entanto a possibilidade de definirprogramas (ou como veremos a frente computacoes) que podem ser executados inde-pendentemente do interpretador. Tais programas necessitam de ser compilados, i.e.,traduzidos para uma linguagem mais proxima da dos processadores.Quando usamos os interpretadores para executar as definicoes de um dado ficheiro, eapos estas serem processadas, entramos num ciclo de operacao, normalmente conhecidopor read, eval, and print que repetidamente

(1) le uma expressao,(2) calcula o seu valor,(3) escreve o resultado.

Ao compilarmos as definicoes de um dado ficheiro, e construıdo um ficheiro executavelque, ao ser executado, executa a computacao definida como main nesse ficheiro.Vejamos um primeiro exemplo muito simples. Suponhamos que no ficheiro de nomeHello.hs colocamos o seguinte texto

Hello.hsmodule Main where

main = putStrLn "Hello"

Deste programa convem salientar que

• o nome do modulo e Main e• existe nesse modulo uma computacao chamada main (de tipo IO ().

A compilacao, num ambiente Unix, faz-se usando o comando ghc aplicado ao nomedo ficheiro. Se nao existirem erros e produzido um executavel com o nome a.out.

$ > ls -l

total 8

-rw-r--r-- 1 jbb unknown 59 11 Feb 11:43 Hello.hs

$ > ghc Hello.hs

$ > ls -l

total 592

-rw-r--r-- 1 jbb unknown 277 11 Feb 11:43 Hello.hi

Page 126: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

122 11. INTERACCAO

-rw-r--r-- 1 jbb unknown 59 11 Feb 11:43 Hello.hs

-rw-r--r-- 1 jbb unknown 3224 11 Feb 11:43 Hello.o

-rwxr-xr-x 1 jbb unknown 288440 11 Feb 11:43 a.out

$ > ./a.out

Hello

$ >

Como podemos ver pelo resultado acima, da compilacao do programa resultam aindaoutros dois ficheiros: um com extensao .hi (de Haskell Interface) e outro com extensao.o (de object file).Podemos ainda especificar qual o nome do ficheiro executavel a ser produzido:

$ > ghc -o Hello Hello.hs

compilation IS NOT required

$ > ls -l

total 1160

-rwxr-xr-x 1 jbb unknown 288440 11 Feb 11:44 Hello

-rw-r--r-- 1 jbb unknown 277 11 Feb 11:43 Hello.hi

-rw-r--r-- 1 jbb unknown 59 11 Feb 11:43 Hello.hs

-rw-r--r-- 1 jbb unknown 3224 11 Feb 11:44 Hello.o

-rwxr-xr-x 1 jbb unknown 288440 11 Feb 11:43 a.out

$ > Hello

Hello

$ >

Note-se a mensagem que e dada pelo compilador: como o ficheiro objecto (Hello.o)tem data de ultima alteracao posterior ao do codigo (Hello.hs, o compilador infereque nao precisa de voltar a compilar o programa mas apenas gerar o executavel.

O construtor de tipos IO que temos vindo a usar para implementar programas in-teractivos e tambem usado para implementar programas que fazem leitura e escritaem ficheiros. De facto, na maioria dos sistemas operativos modernos a interaccao evista como uma caso particular da escrita e leitura de ficheiros. O teclado (isto e,o dispositivo de entrada de dados) esta associado ao ficheiro stdin enquanto que oecran (isto e, o dispositivo de saıda de dados) esta associado ao ficheiro stdout.As operacoes que existem definidas para interaccao nao sao mais do que casos partic-ulares de outras definidas sobre ficheiros genericos. A unica diferenca e a ”burocracia”adicional que e necessaria para identificar qual o ficheiro que vai ser lido ou escrito.A totalidade das operacoes sobre ficheiros pode ser consultada no modulo IO. Dentrodestas, um primeiro grupo serve para escrever e ler a totalidade de um ficheiro:

writeFile :: FilePath -> String -> IO ():readFile :: FilePath -> IO String:appendFile :: FilePath -> String -> IO ():

A abreviatura FilePath para armazenar nomes de ficheiros corresponde ao tipo String.

Page 127: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

11. INTERACCAO 123

O seguinte programa numera as linhas de um dado ficheiro.

module Main wheremain = do{ putStr "Nome do ficheiro de entrada: "; i <- getLine; putStr "Nome do ficheiro de saida: "; o <- getLine; input <- readFile i; writeFile o (transforma input)}

transforma s = unlines (zipWith f (lines s) [1..])where f l n = (show n) ++ (’\t’:l)

Um segundo grupo de operacoes sobre ficheiros envolve a abertura e fecho de um dadoficheiro (as operacoes de open e close) e que sao usadas para com um controlo maisfino, proceder a leitura e escrita de caracteres num ficheiro. Usando estas operacoes epossıvel ter varios ficheiros abertos ao mesmo tempo e intercalar operacoes de escritae leitura nesses varios ficheiros. Deste grupo destacam-se as seguintes operacoes:

• openFile :: FilePath -> IOMode -> IO Handle paa abrir um dado ficheiro.Deve ser fornecido o modo em que o ficheiro vai ser aberto (textttReadMode,WriteMode, AppendMode ou ReadWriteMode).• hClose :: Handle -> IO () para fechar um ficheiro• hFileSize :: Handle -> IO Integer que determina o tamanho de um

ficheiro.• hGetChar :: Handle -> IO Char• hGetContents :: Handle -> IO String• hGetLine :: Handle -> IO String• hPutChar :: Handle -> Char -> IO ()• hPutStr :: Handle -> String -> IO ()• hPutStrLn :: Handle -> String -> IO ()

Page 128: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao
Page 129: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

CAPITULO 12

Modulos

Nas definicoes apresentadas ate aqui, temos usado a palavra module no inıcio de cadaum dos nossos programas sem nunca discutirmos a razao disto nem, principalmente,tirarmos partido dos mecanismos que estao associados ao uso de modulos.Um modulo define um conjunto de classes, de tipos de dados, de valores, etc, de formaa poderem ser usados (importados) por outros modulos. Alem disso e possıvel definirquais destas entidades estarao visıveis nos modulos que o importem.Ja nos deparamos com esta nocao de visibilidade de uma determinada entidade.Relembremos duas possibilidades de definicao da funcao de ordenacao de uma lista(vista na pagina 55)

isort [] = []isort (x:xs) = insert x (isort xs)

insert a [] = [a]insert a (b:bs)| (a <= b) = a:b:bs| otherwise = b:(insert a bs)

isort [] = []isort (x:xs) = insert x (isort xs) whereinsert a [] = [a]insert a (b:bs)| (a <= b) = a:b:bs| otherwise = b:(insert a bs)

A diferenca entre estas duas definicoes e que, na primeira definicao, sao definidasduas funcoes (isort e insert) ao mesmo nıvel, enquanto que na segunda a funcaoinsert e uma funcao local a definicao de isort. Isto significa que, neste caso, a funcaoinsert nao e visıvel fora do contexto da definicao de isort.

125

Page 130: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

126 12. MODULOS

O uso de modulos, para alem de proporcionar uma maior ”arrumacao” das variasdefinicoes, permite controlar de uma forma sistematica, a visibilidade das varias enti-dades definidas.A estrutura sintactica de um modulo Haskell e a seguinte:

• Em primeiro lugar o cabecalho, constituıdo pela palavra module, o identifi-cador do modulo (cuja primeira letra tem de ser maiuscula), a lista de enti-dades que esse modulo exporta (entre parentesis curvos) e a palavra where.• Segue-se a lista das importacoes que esse modulo faz. Cada uma destas

corresponde a uma directiva de import.• Finalmente, seguem-se as definicoes das varias entidades do modulo (funcoes,

tipos, abreviaturas, classes e instancias, etc).

Vejamos entao, e em primeiro lugar, o mecanismo de exportacao.A lista de entidades a exportar pode conter qualquer entidade que seja definida nessemodulo ou que seja importada por esse modulo. Se a lista de entidades a exportar forvazia entao todas as entidades (funcoes, tipos de dados, declaracoes de classes e deinstancias) definidas nesses modulo sao exportadas. Neste caso as entidades que estemodulo importa de outros nao sao exportadas.Para alem das definicoes de valores (incluindo as funcoes) podem ser exportados outrasentidades:

• tipos de dados (declarados como data). Neste caso ha tres formas de fazera exportacao do tipo de dados T:

– exportando T fica disponıvel o tipo T mas nenhum dos seus construtores;– exportando T (c1,...,cn) ficam disponıveis tambem os construtores

explicitados;– exportando T (..) ficam disponıveis o tipo T bem como todos os seus

construtores.• abreviaturas de tipos (declarados como type).• classes e suas operacoes (declaradas como class). Tal como na exportacao

de tipos de dados, ha aqui tres formas de fazer a exportacao da classe C:– exportando C fica disponıvel a classe C mas nenhuma das suas operacoes;– exportando C (f1,...,fn) ficam disponıveis tambem as operacoes ex-

plicitadas;– exportando C (..) ficam disponıveis a classe C bem como todos as suas

operacoes.• modulos

Vejamos agora o mecanismo de importacao.A importacao de um modulo faz-se antes de qualquer definicao local. A importacaode um modulo torna acessıveis todas ou algumas das entidades exportadas por essemodulo.

Page 131: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

12. MODULOS 127

A forma mais simples de importar o modulo M e usar a declaracao

import M

Desta forma, todas as entidades exportadas pelo modulo M ficam visıveis. Alem disso,e para uma qualquer entidade e exportada por M, esta pode ainda ser referenciada porM.e. Esta forma de referenciar a entidade e diz-se qualificada.Podemos ainda fazer uma importacao qualificada com a declaracao

import qualified M

Nesta caso, apesar de todas as entidades exportadas por M estarem disponıveis, apenasas podemos referenciar de uma forma qualificada.E possıvel seleccionar apenas algumas das entidades do modulo em causa. Isto faz-sede uma forma analoga a exportacao, i.e., enumerando as entidades a serem importadas.Esta enumeracao segue as mesmas regras da importacao, permitindo, por exemplo, aimportacao de um tipo de dados com apenas alguns construtores.Uma outra possibilidade que o mecanismo de importacao disponibiliza e a renomeacaode modulos. Para importar o modulo A renomeando-o para B usa-se a declaracao

import A as B

Finalmente, a importacao de modulos pode explicitar quais das entidades de ummodulo que nao devem ser importadas. Isto obtem-se usando a directiva hiding.

Para melhor entender o mecanismo de importacao, suponhamos que o modulo A ex-porta as entidades x e y. Na tabela seguinte mostra-se o resultado de diferentesinstrucoes de importacao deste modulo:

Declaracao Entidades disponıveisimport A x, y, A.x, A.yimport A ()import A (x) x, A.ximport qualified A A.x, A.yimport qualified A ()import qualified A (x) A.ximport A hiding () x, y, A.x, A.yimport A hiding (x) y, A.yimport qualified A hiding (x) A.yimport A as B x, y, B.x, B.yimport A as B (x) x, B.ximport qualified A as B B.x, B.y

Vejamos agora uma das principais aplicacoes de todo este mecanismo de controlo dasentidades visıveis proporcionado pelo uso de modulos – a implementacao de um tipoabstracto de dados.

Page 132: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

128 12. MODULOS

module Stack wheredata Stack a = Empty | S a (Stack a)push x s = S x spop (S _ s) = stop (S t _) = tempty = Empty

Um modulo que importe este tem ao seu dispor a definicao do tipo de dados Stack.Pode, por isso, implementar outras operacoes sobre este tipo. Se em alguns casosisto e uma vantagem, outros ha em que o autor de tal modulo quer impedir que istoaconteca. So dessa forma se pode garantir que uma mudanca na representacao do tipode dados usado, nao afectara o funcionamento dos modulos que importem este.Para obter esta independencia, na lista das entidades exportadas, nao incluımos osconstrutores do tipo.

module Stack (Stack, push, pop, top, empty) wheredata Stack a = Empty | S a (Stack a)push x s = S x spop (S _ s) = stop (S t _) = tempty = Empty

Um modulo que importe este ultimo nao notara a diferenca se o modulo for mudadopara

module Stack (Stack, push, pop, top, empty) wheredata Stack a = S [a]push x (S s) = S (x:s)pop (S (_:s) = stop (S (t:_) = tempty = S []

Page 133: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

CAPITULO 13

Arvores binarias de procura

No Capıtulo 9 definimos arvores como forma de estruturar a informacao. Destas, haum caso particular que e especialmente vantajoso quando o que esta em causa e aprocura frequente de elementos.

Relembremos a definicao da funcao lookup (pre-definida em Haskell):

lookup :: Eq a => a -> [(a,b)] -> Maybe blookup _ [] = Nothinglookup x ((c,i):resto)| x == c = Just i| otherwise = lookup x resto

Esta funcao procura um dado elemento (e costume chamar a estes elementos aschaves) numa lista, e caso o encontre, devolve a informacao correspondente a essachave. Como nao estamos a assumir qualquer tipo de estrutura extra na lista, estaprocura tem de ser feita de uma forma exaustiva – so conseguimos concluir que talchave nao existe depois de precorrer toda a lista.Uma possıvel optimizacao desta funcao passa por assumir que os elementos da listaestao ordenados (por ordem crescente da chave). A funcao de procura e entao:

lookup’ :: Ord a => a -> [(a,b)] -> Maybe blookup’ _ [] = Nothinglookup’ x ((c,i):resto)| x == c = Just i| x < c = Nothing| otherwise = lookup’ x resto

Neste caso, mal encontramos um par em que a chave e maior do que a que procuramos,concluımos que tal chave nao existe (todas as outras sao maiores do que essa!).

As arvores binarias podem ser vistas como uma genralizacao das listas. A funcaocorrespondente a funcao lookup no caso das arvores define-se da seguinte forma.

129

Page 134: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

130 13. ARVORES BINARIAS DE PROCURA

data Nodo x = Vazia | Nodo x (Nodo x) (Nodo x)

lookupA :: Eq a => a -> Nodo (a,b) -> Maybe blookupA _ V = NothinglookupA x (N (c,i) e d)| x == c = Just i| otherwise =case (lookup x e) ofNothing -> looup x dJust i’ -> Just i’

Esta solucao padece dos mesmos problemas da funcao lookup – so depois de pre-corrermos toda a arvore e que podemos concluır que uma dada chave nao existe naarvore.A optimizacao correspondente a que fizemos com as listas ordenadas e definir arvoresde procura que sao a generalizacao do conceito de lista ordenada.Uma arvore binaria e de procura quando

• e vazia ou•

– a raız e maior ou igual a todos os elementos da sub-arvore esquerda,– a raız e menor ou igual a todos os elementos da sub-arvore direita e– ambas as sub-arvores sao arvores binarias de procura.

As arvores seguintes sao arvores binarias de procura.

14

10

1 13

20

27

14

20

16 27

35

43

5240

Por outro lado, as arvores seguintes nao sao de procura.

Page 135: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

13. ARVORES BINARIAS DE PROCURA 131

14

10

1 15

20

16 27

14

10

1 15

20

16 27

35

43

5240

• No caso da arvore da esquerda, existe um elemento na sub-arvore esquerdaque e maior do que a raız.• No caso da outra arvore, apesar de todos os elementos da sub-arvore esquerda

serem menores do que a raız e de todos os elementos da sub-arvore direitaserem maiores do que a raız, a sub-arvore esquerda nao e uma arvore binariade procura.

Vejamos agora a funcao que, dada uma arvore (de pares), calcula o valor associado aum dado elemento:

lookupArv :: (Ord a) => a -> Nodo (a,b) -> Maybe blookupArv _ Vazia = NothinglookupArv x (Nodo (a,b) e d)| x < a = lookupArv x e| x > a = lookupArv x d| otherwise = Just b

Podemos ver que a procura de um dado elemento vai descendo pela arvore ate encon-trar o elemento ou atingir uma folha. Daı que esta descida esteja limitada ao peso daarvore.Vejamos entao qual a relacao entre o peso e o numero de nodos de uma arvore.

• Uma arvore com peso 1 tem um unico elemento.• Uma arvore com peso 2 tem no mınimo 2 elementos e no maximo 3

Page 136: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

132 13. ARVORES BINARIAS DE PROCURA

• Uma arvore com peso 3 tem no mınimo 3 elementos e no maximo 7

• Uma arvore com peso 4 tem no mınimo 4 elementos e no maximo 15

• Uma arvore com peso n tem no mınimo n elementos e no maximo 2n − 1

Invertendo esta correspondencia, temos que

• Uma arvore com 1 elemento tem peso 1.• Uma arvore com 3 elementos tem peso mınimo 2 e maximo 3.• Uma arvore com 7 elementos tem peso mınimo 3 e maximo 7.• Uma arvore com 15 elementos tem peso mınimo 4 e maximo 15.• Uma arvore com n elementos tem peso mınimo log2(n+ 1) e maximo n.

Isto significa que a funcao de procura de uma arvore com n elementos esta limitadaentre um valor mınimo de log2(n + 1) e um valor maximo de n. E de notar que osvalores destes limites, particularmente quando o numero de elementos e muito grande,diferem muito (a tıtulo de exemplo, veja-se que log2 1000000000 = 30). O caso maisfavoravel e entao quando a arvore se encontra balanceada. Formalmente, uma arvorediz-se balanceada quando

• e vazia ou• nao sendo vazia, a diferenca entre os pesos das suas sub-arvores e inferior a

2 e ambas estas sub-arvores estao balanceadas1.

O nosso objectivo e definir uma funcao que faca a insercao de um elemento numaarvore de procura balanceada mantendo esta propriedade no resultado.Vejamos em primeiro lugar uma definicao de uma funcao de insercao numa arvorebinaria de procura que nao tem esta preocupacao.

1Vimos atras que o numero maximo de elementos de uma arvore com peso n e 2n + 1. Usandoesta definicao de arvore balanceada, e interessante calcular qual o numero mınimo de elementos queuma arvore balanceada com peso n pode ter.

Page 137: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

13. ARVORES BINARIAS DE PROCURA 133

insereA :: (Ord a) => a -> (Nodo a) -> (Nodo a)insereA x V = N x V VinsereA x arv@(N r e d)| x == r = arv| x < r = N r (insereA x e) d| x > r = N r e (insereA x d)

Veja-se entao a sucessiva insercao dos elementos 7,6,2,5,1,3,4 numa arvore inicial-mente vazia (e por isso mesmo balanceada):

7

6

2

1 5

3

4

7

6

2

1 5

3

7

6

2

1 5

7

6

2

5

7

6

2

7

6

7

Se por outro lado a ordem de insercao tivesse sido 4,2,6,5,7,1,3, a arvore resutanteteria resultado balanceada.

Page 138: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

134 13. ARVORES BINARIAS DE PROCURA

4

2

1 3

6

5 7

4

2

1

6

5 7

4

2 6

5 7

4

2 6

5

4

2 6

4

2

4

Uma solucao para definir a funcao pretendida envolve estar constantemente a calcularos pesos das varias sub-arvores de forma a poder reorganizar a arvore tornando-abalanceada. Esta solucao e no entanto muito pouco eficiente. Uma outra formaconsiste em guardar em cada nodo da arvore a diferenca entre os pesos das suas sub-arvores. Se a arvore for balanceada sabemos que estes pesos so podem ter tres valorespossıveis: (−1) se a sub-arvore esquerda e menos pesada do que a direita, 0 se os pesossao iguais e (1) se a direita e mais pesada. Daı a seguinte definicao:

data Balanco = Esq | Bal | Dir

type ArvBal a = BTree (Balanco,a)

Para definir a funcao de insercao balanceada vamos precisar de definir uma funcaoauxiliar que alem de inserir um elemento numa arvore balanceada (retornando umaarvore balanceada), nos informe se o peso da arvore aumentou.

insereBal :: (Ord a) => ArvBal a -> a -> ArvBal ainsereBal arv x = snd (insereBalAux arv x)

A funcao insereBalAux retorna um par: a arvore resultante da insercao e um valorbooleano que sera verdadeiro se a arvore resultante tem peso maior do que o da inicial.No caso mais simples (insercao de um elemento numa arvore vazia) o valor retornadoe uma arvore com um unico elemento, e por isso mesmo com balanco perfeito; alemdisso neste caso a arvore aumenta de peso.

insereBalAux Vazia x = (True, Nodo (Bal,x) Vazia Vazia)

Vejamos agora o outro caso. Em primeiro lugar devemos decidir em qual das sub-arvores a insercao sera feita.

Page 139: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

13. ARVORES BINARIAS DE PROCURA 135

insereBalAux arv@(Nodo (b,r) e d) x| x == r = (False, arv)| x < r = insereEsq arv x| x > r = insereDir arv x

Vejamos entao o caso em que a insercao se faz na sub-arvore esquerda:

insereEsq (Nodo (Esq,r) e d) x =if aumentouthen balancoE r e’ delse (False, Nodo (Esq,r) e’ d)where (aumentou,e’) = insereBalAux e x

insereEsq (Nodo (Bal,r) e d) x =if aumentouthen (True, Nodo (Esq,r) e’ d)else (False, Nodo (Bal, r) e’ d)where (aumentou,e’) = insereBalAux e x

insereEsq (Nodo (Dir,r) e d) x =if aumentouthen (False, Nodo (Bal,r) e’ d)else (False, Nodo (Dir,r) e’ d)where (aumentou,e’) = insereBalAux e x

A funcao balancoE recebe como argumentos osconstituintes (a raiz e ambas as sub-arvores)de uma arvore que esta desbalanceada. Senaovejamos. A arvore inicial tinha um factor debalanco Esq, significando que a sua sub-arvoreesquerda era mais pesada do que a direita(como sabemos, esta diferenca e no maximo 1).Alem disso a insercao foi feita na sub-arvoreesquerda e esta aumentou de peso. Daı queseja necessario fazer algum rearranjo da arvorepara manter o seu balanceamento. E esse o pa-pel da funcao balancoE. Graficamente temos asituacao que se mostra ao lado.

r

e

dn+2n

Para podermos tomar a accao reparadora conveniente devemos analizar com maisdetalhe qual dos ramos da arvore esquerda e que e o causador do desbalanceamento.Se o balanco desta sub-arvore for Esq, veja-se no seguinte esquema qual a accaoreparadora a efectuar.

Page 140: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

136 13. ARVORES BINARIAS DE PROCURA

da

db

b

a

n

n

b

a

eb

n+1

db dan

eb

n+1

balancoE a (Nodo (Esq,b) eb db) da= (False, (Nodo (Bal,b) eb (Nodo (Bal,a) db da)))

Esta accao reparadora tem que ter em conta dois factores: em primeiro lugar queremosvoltar a repor o balanceameno da arvore. Mas devemos ainda manter a arvore comouma arvore de procura. Note-se que em termos de ordenacao temos que

eb ≤ 2b ≤ 3db ≤ 4a ≤ 5da

Estamos a usar o sımbolo ≤ de uma forma pouco usual pois estamos a compararelementos com conjuntos de elementos. Ao usarmos esta notacao queremos significarque os elementos do conjunto sao todos menores ou iguais ao elemento em causa.No outro caso ainda temos que analizar com mais detalhe qual a sub-arvore que esta acontribuir para este aumento de peso. Mais uma vez, por simples inspeccao do balancodesta sub-arvore, podemos tomar as necessarias decisoes. Isto pode ser feito apenaspor inspeccao do balanco da raiz dessa arvore:

Page 141: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

13. ARVORES BINARIAS DE PROCURA 137

• No caso desse balanco ser Esq

da

eb

b

a

n

c

ec

dcn

ndaeb

b a

c

ec

dc

balancoE a (Nodo (Dir, b) eb (Nodo (Esq, c) ec dc)) da= (False, (Nodo (Bal, c) (Nodo (Bal, b) eb ec)

(Nodo (Dir, a) dc da)))

• No caso desse balanco ser Dir

da

eb

b

a

n

c

ec

dcn

ndaeb

b a

c

ec

dc

balancoE a (Nodo (Dir, b) eb (Nodo (Dir, c) ec dc)) da= (False, (Nodo (Bal, c) (Nodo (Esq,b) eb ec)

(Nodo (Bal, a) dc da)))

Page 142: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

138 13. ARVORES BINARIAS DE PROCURA

• No caso desse balanco ser Bal

b

a

c

b a

c

balancoE a (Nodo (Dir, b) eb (Nodo (Bal, c) ec dc)) da= (False, (Nodo (Bal, c) (Nodo (Bal,b) eb ec)

(Nodo (Bal, a) dc da)))

O caso em que o desbalanceamento ocorre no lado direito da arvore e perfeitamentesimetrico a este.Aqui o desbalanceamento ocorre quando ao inserir um elemento na sub-arvore direitaesta aumenta de peso e por sua vez a arvore inicial ja se encontrava com um balancodireito.

insereDir (Nodo (Dir,r) e d) x =if aumentouthen balancoD r e d’else (False, Nodo (Dir,r) e d’)where (aumentou,d’) = insereBalAux d x

insereDir (Nodo (Bal,r) e d) x =if aumentouthen (True, Nodo (Dir,r) e d’)else (False, Nodo (Bal, r) e d’)where (aumentou,d’) = insereBalAux d x

insereDir (Nodo (Esq,r) e d) x =if aumentouthen (False, Nodo (Bal,r) e d’)else (False, Nodo (Esq,r) e d’)where (aumentou,d’) = insereBalAux d x

Mais uma vez, e de acordo com o factor de balanco das varias sub-arvores, podemostomar as medidas correctivas de forma a repor o balanceamento da arvore. Tal como

Page 143: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

13. ARVORES BINARIAS DE PROCURA 139

anterirmente, a funcao balancoD recebe como argumentos os constituintes de umaarvore desbalanceada: a raiz e as duas sub-arvores balanceadas.

• No caso do balanco da sub-arvore direita ser Dir

ea

eb

b

a

n

n

db

n+1ea eb

b

a

db

n+1

n

balancoD a ea (Nodo (Dir,b) eb db)= (False, Nodo (Bal,b) (Nodo (Bal,a) ea eb) db)

• No caso do balanco da sub-arvore direita ser Esq, temos 3 situacoes possıveis,depedendo do balanco da sub-arvore esquerda desta. Se este for Dir

dbea

a b

c

ec

dc

n

n

n

ea

db

b

a

c

dc

ec

balancoD a ea (Nodo (Esq, b) (Nodo (Dir, c) ec dc) db)= (False, (Nodo (Bal, c) (Nodo (Esq, a) ea ec)

(Nodo (Bal, b) dc db)))

Page 144: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

140 13. ARVORES BINARIAS DE PROCURA

• Se for Esq

dbea

a b

c

ec

dc

n

n

n

ea

db

b

a

c

dc

ec

balancoD a ea (Nodo (Esq, b) (Nodo (Esq, c) ec dc) db)= (False, (Nodo (Bal, c) (Nodo (Bal,a) ea ec)

(Nodo (Dir, b) dc db)))

• E finalmente se for balanceada (e por isso msmo uma arvore singular!)

b

a

c

a b

c

balancoD a ea (Nodo (Esq, b) (Nodo (Bal, c) ec dc) db)= (False, (Nodo (Bal, c) (Nodo (Bal,a) ea ec)

(Nodo (Bal, b) dc db)))

Vejamos agora um exemplo da insercao sucessiva dos elementos 1,2,3,4,5,6,7 numaarvore inicialmente vazia.

Page 145: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

13. ARVORES BINARIAS DE PROCURA 141

4

2

1 3

6

5 7

4

2

1

5

6

2

1 4

3 5

2

1 3

4

2

1 3

1

2

1

3

Uma forma de nos convencermos que esta funcao esta de facto bem construıda e pelocalculo do peso das arvores resultantes.

depth Vazia = 0depth (Nodo _ e d) = 1 + (max (depth e) (depth v))

Main> depth (foldr insereBal Vazia [1..2ˆ10 -1])

10

Main> depth (foldr insereBal Vazia [1..2ˆ12 -1])

12

Main> depth (foldr insereBal Vazia [1..2ˆ13 -1])

13

Exercıcios

13.1 Considere a seguinte definicao da funcao que remove um elemento de uma arvorebinaria de procura.

remove x Vazia = Vaziaremove x (Nodo r e d)| x < r = Nodo r (remove x e) e| x > r = Nodo e e (remove x d)| x == r = removeRaiz (Nodo r e d)

removeRaiz (Nodo _ Vazia d) = dremoveRaiz (Nodo _ e Vazia) = eremoveRaiz (Nodo _ e d) = Nodo a e’ d where(a,e’) = maior e

Page 146: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

142 13. ARVORES BINARIAS DE PROCURA

maior (Nodo x e Vazia) = (x,e)maior (Nodo x e d) =let (a,d’) = maior din (a, Nodo x e d’)

A funcao auxiliar maior calcula, para uma dada arvore nao vazia, o seu maiorelemento e essa arvore sem esse elemento.

Defina agora uma funcao que remove um elemento de uma arvore de procurabalanceada, mantendo o balanceamento da arvore.

Page 147: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

CAPITULO 14

Funcoes finitas

Uma classe importante de problemas passa pelo uso e manutencao de correspondenciasunıvocas entre um conjunto (ou tipo) de elementos chamados chaves e um outro aque chamaremos informacao. Sao inumeros os exemplos que podemos arranjar paraestas associacoes unıvocas atraves de uma chave. Desde aplicacoes como as que fazema gestao de contas bancarias (e aqui cada cliente tem um numero – a sua chave – aque estao associadas contas, e em que estas sao por sua vez tambem univocamenteidentificadas por um numero de conta), ao numero de identificacao fiscal ou o numerode bilhete de identidade.Embora possamos conceber varias implementacoes possıveis para este tipo de fun-cionalidade, vamos comecar por definir uma classe que especifique o genero de funcoesque temos em mente.Vejamos entao quais as operacoes que pretendemos que estejam disponıveis para estascorrespondencias:

• determinar se uma funcao finita esta definida para uma dada chave;• calcular o domınio e contradomınio de uma correspondencia;• determinar, para valores definidos da funcao, qual a informacao associada a

uma dada chave;• acrescentar uma nova associacao a uma funcao finita;• restringir o domınio ou codomınio de uma dada funcao.

Finalmente e ja fora da definicao da classe, vamos querer definir funcoes de composicaoe fusao de funcoes finitas. Veremos na altura qual a vantagem em definir estas funcoesfora da definicao da classe.Transcrevendo em sintaxe Haskell os pontos acima, obtemos a seguinte definicao daclasse das funcoes finitas.

class FFinita m wheredefinido :: (Ord a,Eq a) => (m a b) -> a -> Booldominio :: (Ord a) => (m a b) -> [a]codominio:: (Ord a) => (m a b) -> [b]valor :: (Ord a) => (m a b) -> a -> Maybe bacrescenta :: (Ord a) => (m a b) -> a -> b -> (m a b)restr :: Ord a => ((a,b) -> Bool) -> m a b -> m a b

143

Page 148: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

144 14. FUNCOES FINITAS

drestr :: Ord a => (a -> Bool) -> m a b -> m a bcdrestr :: Ord a => (b -> Bool) -> m a b -> m a b

definido f x = elem x (dominio f)drestr p = restr (p . fst)cdrestr p = restr (p . snd)

Como vemos esta classe e, tal como acontecia com os conjuntos, uma classe de con-strutores. A restricao que impusemos de as chaves serem da classe Ord prende-se comas implementacoes mais eficientes que ja referimos atras (listas oerdenadas e arvoresbinarias de procura). Mas, de um ponto de vista estritamente de especificacao, de-verıamos ter colocado como restricao apenas que as chaves sejam da classe Eq.

Vejamos entao uma primeira implementacao – usando listas de pares em que o primeiroelemento corresponde as chaves e o segundo a informacao associada as chaves.

data (Ord c) => LPares c i = P [(c,i)]

instance FFinita LPares wheredominio (P l) = map fst lcodominio (P l) = map snd lvalor (P l) x = lookup x lacrescenta (P l) c i = P (insere c i l) whereinsere a b [] = [(a,b)]insere a b ((c,i):r)| a < c = (a,b):(c,i):r| a == c = (a,b):r| otherwise = (c,i):(insere a b r)

restr p (P l) = P (filter p l)

Uma outra possibilidade de implementacao de funcoes finitas e o uso de arvoresbinarias de procura.

data Ord c =>STree c i = Vazia | Nodo (c,i) (STree c i) (STree c i)

instance FFinita STree wheredominio Vazia = []dominio (Nodo (c,i) e d)= c:((dominio e) ++ (dominio d))codominio (Nodo (c,i) e d)= i:((codominio e) ++ (codominio d))valor Vazia _ = Nothing

Page 149: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

14. FUNCOES FINITAS 145

valor (Nodo (c,i) e d) x| x == c = Just i| x < c = valor e x| x > c = valor d xacrescenta Vazia c i = Nodo (c,i) Vazia Vaziaacrescenta (Nodo (x,y) e d) c i| x == c = Nodo (c,i) e d| x < c = Nodo (x,y) (acrescenta e c i) d| x > c = Nodo (x,y) e (acrescenta d c i)restr = filterA

As implementacoes das funcoes de restricao (do domınio e codomınio) estao baseadasnuma funcao de filtragem de arvores. Tal funcao, que pode ser entendida como umagenralizacao da funcao filter, recebe como argumento um predicado (i.e., uma funcaobooleana) e uma arvore e devolve a arvore apenas com os elementos que satisfazemesse predicado.Uma forma de definir esta funcao e basea-la realmente na funcao filter: comecamospor converter a arvore numa lista, de seguida filtramos os elementos com a funcaofilter e, finalmente, reconstruımos a arvore.

filterA :: (Ord c)=> ((c,i) -> Bool) -> (STree c i) -> STree c i

filterA p a = reconstroi (filter p (arv2lista a))

A forma de definir a funcao reconstroi depende da forma como foi definida a funcaoarv2lista. Uma definicao eficiente destas funcoes e usar uma estrategia pre-orderpara a conversao da arvore em lista.

arv2lista Vaziaazia = []arv2lista (Nodo x e d) = x:((arv2lista e) ++ (arv2lista d))

Desta forma, a lista resultante tem como cabeca o elemento da raız e depois doisblocos de elementos: em primeiro lugar os que sao menores do que a raız seguidos dosque sao maiores ou iguais. Sabendo isto, a funcao reconstroi pode ser definida daseguinte forma:

reconstroi [] = Vreconstroi (h:t) = Nodo h (reconstroi e) (reconstroi d) where(e,d) = split h tsplit x [] = ([],[])split x (a:aa)| a >= x = ([],(a:aa))| otherwise = let (p,q) = split x aa in (a:p,q)

Page 150: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

146 14. FUNCOES FINITAS

Podemos tentar definir a funcao de filtragem de elementos de uma arvore sem recorrera esta passagem intermedia por uma lista.Para filtrar uma arvore nao vazia devemos, depois de filtrar ambas as sub-arvores,decidir o que fazer com a raız da arvore.Se esse elemento satisfizer o predicado em causa, o resultado e facil de obter: bastaconstruir uma arvore com essa raız e as sub-arvores resultantes.No entanto, se o predicado nao for satisfeito pelo elemento da raız devemos determinarum elemento para ser armazenado como raız. Note-se que o invariante das arvoresbinarias de procura tem de ser mantido. Por isso ha duas escolhas possıveis: o maiordos elementos da sub-arvore esquerda ou o menor dos elementos da sub-arvore direita.Em qualquer das escolhas devemos, depois de seleccionado o elemento que vai aparecerna raız, remove-lo da sub-arvore correspondente.

filterA p Vazia = VaziafilterA p (Nodo r e d)| p r = Nodo r (filterA p e) (filterA p d)| otherwise =case (filterA2 p e) ofVazia -> filterA2 p da -> let me = maximoSTree a

e’ = removeMax ain Nodo me e’ (filterA2 p d)

maximoSTree (Nodo r e Vazia) = rmaximoSTree (Nodo r e d) = maximoSTree d

removeMax (Nodo r e Vazia) = eremoveMax (Nodo r e d) = (Nodo r e (removeMax d))

Para definirmos esta funcao de uma forma mais eficiente vamos comecar por definiruma funcao auxiliar filterAux que, recebendo os mesmos parametros, calcula umpar em que

• a primeira componente e a arvore filtrada,• a segunda componente e ela propria um par constituıdo pelo maior elemento

da arvore e pela arvore resultante de retirar o maior elemento.

Note-se que, como a arvore filtrada pode ser vazia, esta segunda componente podenao estar definida; vamos por isso usar o construtor Maybe.

filterAux p Vazia = (Vazia, Nothing)filterAux p (Nodo r e d)| p r =case md of

Page 151: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

14. FUNCOES FINITAS 147

Nothing -> (Nodo r e’ Vazia, Just (r, e’))Just (md’,d’’)-> (Nodo r e’ d’, Just (md’, Nodo r e’ d’’))

| otherwise =case me ofNothing -> (d’,md)Just (me’,e’’) ->case md ofNothing -> (Nodo me’ e’’ d’, Just (me’,e’’))Just (md’,d’’)-> (Nodo me’ e’’ d’, Just (md’, Nodo me’ e’’ d’’))

where(e’,me) = filterAux p e(d’,md) = filterAux p d

A definicao da funcao filterA e agora facil de escrever.

filterA p a = fst (filterAux p a)

Vejamos agora como calcular a aplicacao da composicao de duas funcoes finitas.

compor :: (FFinita f, FFinita g)=> f b c -> f a b -> a -> Maybe b

compor f g x = case valor g x ofNothing -> NothingJust y -> valor f y

Page 152: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao
Page 153: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

CAPITULO 15

Grafos

Suponhamos que pretendemos armazenar a informacao referente as ligacoes rodoviariasentre varias localidades. Os problemas que se colocam no acesso a tal informacao sao:

• determinar se existe ou nao ligacao directa entre duas localidades e em casoafirmativo, qual o comprimento de tal ligacao;• determinar o caminho mais curto entre duas localidades;• determinar o conjunto mınimo de ligacoes que asseguram a conectividade

entre todas as localidades;

Vejamos um exemplo. Dadas as ligacoes descritas no diagrama seguinte

A

E

CB

D

G HF

3

5

1

6

13

2

62

temos que

• Existe uma ligacao directa entre as localidades A e C.• O caminho mais curto entre essas localidades corresponde ao caminho A,B,D,G,C.• Algumas das ligacoes sao redundantes, isto e, se eliminarmos por exemplo a

ligacao directa de A a B continuamos a ter a mesma conectividade.

149

Page 154: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

150 15. GRAFOS

Este tipo de problemas esta associado a um area das Ciencias da Computacao normal-mente conhecida por Teoria de Grafos1. Um grafo, na sua formulacao mais abstracta,e composto por um conjunto de vertices (ou nodos) e um conjunto de arestas (ouarcos). No nosso exemplo, os vertices correspondem as localidades e as arestas asligacoes rodoviarias. A cada aresta estao associadas as suas extermidades. Os grafoscaracterizam-se de acordo com a informacao que esta associada a cada aresta:

• Nos grafos orientados as suas duas extermidades chamam-se origem e des-tino.• Nos grafos pesados existe associado a cada aresta um peso (normalmente

um numero).• Se puder existir mais do que uma aresta com a mesma origem e destino,

dizemos que se trata de um muti-grafo.

Antes de apresentarmos uma solucao para a representacao da informacao relevantevamos esbocar a definicao de uma das funcoes que pretendemos disponıvel sobre estainformacao. Tomemos por exemplo a funcao que determina se existe um caminho(directo ou nao) entre duas localidades. Para poder definir esta funcao vamos assumircomo definido o tipo G v a para representar grafos cujos vertices sao do tipo v e asarestas do tipo a.Veremos mais adiante alternativas para definir este tipo.Uma primeira abordagem ao problema de determinar se existe um caminho entre doisvertices consiste no seguinte: existe um caminho de uma dada origem para um dadodestino se

• esse destino e adjacente a origem (i.e., existe uma aresta com essa origem eesse destino) ou,• existe um vertice adjacente a origem para o qual existe um caminho ate ao

destino.

1Um dos primeiros problemas sobre grafos e descrito como um mito urbano da actual cidade deKalingrado (a altura chamada Konigsberg). Nesta cidade existiam duas ilhas ligadas entre si e comas margens do rio por 7 pontes tal como se descreve abaixo.

Era passatempo dos habitantes locais, descobrir um circuito que passasse uma unica vez por todas

as pontes regressando a origem. Em 1728 Euler acabou com o passatempo, provando que tal tarefae impossıvel.

Page 155: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

15. GRAFOS 151

haCaminho :: G v a -> v -> v -> BoolhaCaminho g origem destino =(adjacente g origem destino) ||haUmCaminho g (adjacentes g origem) destino

Apesar de ainda nao termos apresentado a definicao da funcao haUmCaminho, podemosja constatar que esta definicao nao esta correcta. Se o grafo em causa contiver ciclos(i.e., caminhos em que a origem e o destino coincidem) podemos ter uma definicaoincorrecta. Vejamos um exemplo.

Ao tentarmos determinar se existe um caminho de A para C, ecomo nao existe nenhuma aresta nestas condicoes, vamos tentarencontrar um caminho de B para C.Ao tentarmos determinar se existe um caminho de B para C, ecomo nao existe nenhuma aresta nestas condicoes, vamos tentarencontrar um caminho de A para C, voltando ao problema inicial.

C

B

A

A solucao usada para evitar este tipo de problemas e, de umas invocacoes para outras,mantermos a informacao sobre quais os vertices que ja foram usados (ou visitados).Vejamos entao como tal estrategia pode ser usada:

haCaminho :: (Eq v) => G v a -> v -> v -> BoolhaCaminho g o d = haCaminhoV [] g o d wherehaCaminhoV v g o d =(adjacente g o d) ||(haUmCaminho (o:v) g (adjacentes g o) d)haUmCaminho v g [] y = FalsehaUmCaminho v g (x:xx) y| x ‘elem‘ v = haUmCaminho v g xx y| otherwise = haCaminhoV v g x y || haUmCaminho v g xx y

Nesta definicao a funcao haCaminho apenas invoca a funcao haCaminhoV que tem umparametro extra – os vertices ja visitados. Na primeira invocacao desta funcao (i.e.,a invocacao feita pela funcao haCaminho) ainda nao ha vertices visitados pelo que oargumento passado e a lista vazia. Em qualquer nova invocacao desta funcao, esteparametro vai sendo aumentado. Trata-se, como ja vimos atras de um acumulador.Note-se a necessidade de introduzir no tipo a restricao Eq v. Isto porque sobre estetipo e efectuada uma operacao que envolve a igualdade – a funcao elem.Desta definicao podemos ainda inferir algumas das funcoes que serao necessarias sobrea representacao de grafos:

• adjacente e a funcao que num dado grafo testa se dois vertices sao adja-centes.

Page 156: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

152 15. GRAFOS

• adjacentes ee a funcao que num dado grafo calcula a lista dos verticesadjacentes a um dado vertice.

Uma generalizacao da funcao haCaminho e a funcao que calcula um caminho entredois vertices. Como se trata de uma funcao parcial (nao esta definida se nao existirnenhum caminho) vamos usar o construtor Maybe para o tipo do resultado. No casode nao existir qualquer caminho a funcao retornara Nothing. Nos outros casos vamosoptar por retornar a lista dos vertices que compoe o caminho.

caminho :: (Eq v) => G v a -> v -> v -> Maybe [v]caminho g o d = caminhoV [] g o d wherecaminhoV v g o d| adjacente g o d = Just [o,d]| otherwise =case (umCaminho (o:v) g (adjacentes g o) d) ofNothing -> NothingJust c -> Just (o:c)

umCaminho v g [] y = NothingumCaminho v g (x:xx) y| x ‘elem‘ v = umCaminho v g xx y| otherwise =case (caminhoV v g x y) ofNothing -> umCaminho v g xx yJust c -> Just c

Mais uma vez podemos notar que as unicas operacoes que precisamos de ter disponıveissobre o tipo G v a sao o calculo dos vertices adjacentes a um dado vertice e o testese dois vertices sao adjacentes.Para finalizar esta primeira categoria de funcoes sobre grafos falta-nos definir a funcaoque calcula o caminho mais curto entre dois vertices. A estrategia mais simples pararesolver este problema consiste consiste em comecar por determinar todos os caminhosentre esses vertices seleccionando depois aquele que tem o menor peso (entende-se porpeso de um caminho a soma dos pesos das arestas que compoe esse caminho).

caminhoMCurto g o d =case (todosOsCaminhos [] g o d) of[] -> Nothingcs -> Just (snd (maisCurto cs))wheremaisCurto [(p,c)] = (p,c)maisCurto ((p,c):pcs) =let (mp,mc) = maisCurto pcsin if p < mp then (p,c) else (mp,mc)

Page 157: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

15. GRAFOS 153

Uma forma alternativa de definir a funcao auxiliar maisCurto seria usando a funcaoque calcula o menor elemento de uma lista nao vazia (minimum). No entanto isto iriaimplicar uma restricao ao tipo dos vertices que a solucao acima nao impoe (sereminstancias da classe Ord). Na solucao aqui apresentada o unico tipo que tem de serinstancia dessa classe e o tipo das arestas.

todosOsCaminhos v g o d =camUnitario g o d ++map (\(p,(x:c)) -> (p+(peso g o x),o:x:c))(outrosC (o:v) g (adjacentes g o) d)wherecamUnitario g o d| (adjacente g o d) = [((peso g o d),[o,d])]| otherwise = []

outrosC _ _ [] _ = []outrosC v g (x:xx) d| (x ‘elem‘ v) = outrosC v g xx d| otherwise= (todosOsCaminhos v g x d) ++ (outrosC v g xx d)

Relativamente as duas outras funcoes definidas atras, esta traz uma outra condicaoextra sobre a representacao de grafos: e necessario existir uma funcao peso que dadoum grafo e dois vertices adjacentes, calcule o peso da aresta correspondente. Mais doque isso o tipo dos pesos das arestas deve ser de um tipo numerico (pois precisamosde somar estes pesos).Se usarmos um interpretador de Haskell podemos ver todas as restricoes que saonecessarias para que esta funcao esteja bem definida:

Grafos> :t caminhoMCurto

caminhoMCurto :: (Ord a, Num a, Eq v) => G v a -> v -> v -> Maybe [v]

Grafos>

As funcoes que apresentamos sobre grafos tem como caracterıstica comum a deter-minacao de informacao acerca de caminhos entre dois dados vertices. E por isso co-mum agrupa-las numa classe de problemas chamada uma origem e um destino. Foramdefinidas dentro desta categoria tres funcoes:

• haCaminho que apenas determina se ha ou nao caminho entre ambos osvertices;• caminho que determina um caminho entre os vertices;• caminhoMCurto que determina o caminho mais curto entre os vertices.

A ultima destas funcoes so faz sentido em grafos pesados. Num grafo nao pesado,podemos associar a cada aresta um peso constante pelo que, nesse contexto a funcaoretornara o caminho com um menor numero de arestas.

Page 158: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

154 15. GRAFOS

Uma outra classe de problemas associados a grafos consiste em fixar apenas a origemdos caminhos. Esta classe de problemas e normalmente chamada uma origem, todosos destinos e as tres funcoes correspondentes as anteriores sao:

• alcancaveis que determina quais os vertices para os quais ha caminho apartir do vertice dado;• arvGer que determina um sub-grafo em que existe um unico caminho do

vertice dado para qualquer um dos vertices alcancaveis desse vertice; estesub-grafo e normalmente chamado arvore geradora do grafo dado.• arvGerCM que determina a arvore geradora de custo mınimo.

Para resolver os dois primeiros problemas vamos precisar de estabelecer uma estrategiade travessia de um grafo a partir de um dado vertice. Dentro das estrategias possıveisdestacam-se

• depth first em que para cada vertice se faz uma travessia a partir de cadaum dos vertices que lhe sao adjacentes.• breadth first em que para cada vertice se comeca por visitar todos os seus

adjacentes, seguidos dos adjacentes destes e assim sucessivamente.

Vamos analisar um exemplo para melhor compreender estas estrategias.

A

B

C

D

E

G

F

H

K

J

I

Ao fazermos uma travessia deste grafo comecando no vertice A, a ordem pela qual osvertices sao visitados seria:

• numa travessia depth first: A,B,D,G,I,J,E,H,C,F,K.• numa travessia breadth first: A,B,C,D,E,F,G,H,K,I,J.

Embora estas estrategias sejam bastante diferentes, vamos ver que existe uma formasemelhante de as implementar.

Page 159: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

15. GRAFOS 155

Vejamos entao como definir a funcao que determina os vertices alcancaveis a partir deum dado vertice seguindo uma estrategia depth first.

alcancaveisD g o = depthF g [] [o]

A funcao depthF recebe como argumentos, para alem do grafo em causa, duas listas:

• a primeira corresponde a lista dos vertices ja visitados e• a segunda corresponde aos vertices que ja foram escalonados para serem

visitados.

A visita de um dado vertice, para alem de o marcar como visitado deve escalonartodos os seus vertices adjacentes para tambem serem visitados. O processo terminaquando ja nao existirem mais vertices escalonados para serem visitados. Neste caso,a lista dos vertices ja visitados contem todos os vertices alcancaveis do vertice inicial.

depthF g v [] = vdepthF g v (o:os)| o ‘elem‘ v = depthF g v os| otherwise =let a = adjacentes g oin depthF g (v ++ [o]) (a ++ os)

Aquilo que nesta definicao caracteriza a estrategia depth first e o escalonamento dosvertices. Ao fazermos a nova invocacao da funcao depthF com a lista a++os estamosa dizer que antes dos vertices que ja estavam escalonados para serem visitados, vis-itaremos primeiro os adjacentes do vertice actual. Numa estrategia breadth first, aoescalonarmos novos vertices para serem visitados, estes sao-no depois dos que ja estaoescalonados.Assim sendo, a unica diferenca entre a travessia depth first e a travessia breadthfirst sera no valor deste parametro da invocacao recursiva.Antes de passarmos a definicao da funcao de travessia breadth first convem notaruma pequena optimizacao que pode ser feita na definicao acima. A funcao depthFusa o conjunto (i.e., a lista) dos vertices visitados como um acumulador. De formaa que este acumulador tenha a sua forma final quando a funcao termina temos queacrescentar os novos vertices no final da lista. Esta operacao de acrescentar no finalda lista e “dispendiosa”: temos que percorrer toda a lista. Uma forma muito maiseconomica de acrescentar um elemento a uma lista e acrescenta-lo no inıcio. Masisso tera como consequencia que acumulador tem os vertices armazenados pela ordeminversa. Para contornar esse problema basta-nos, no final, inverter esse acumuladorpara dar o resultado final.

alcancaveisD g o = depthF g [] [o]alcancaveisB g o = breadthF g [] [o]

Page 160: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

156 15. GRAFOS

depthF g v [] = reverse vdepthF g v (o:os)| o ‘elem‘ v = depthF g v os| otherwise =let a = adjacentes g oin depthF g (o:v) (a ++ os)

breadthF g v [] = reverse vbreadthF g v (o:os)| o ‘elem‘ v = depthF g v os| otherwise =let a = adjacentes g oin breadthF g (o:v) (os ++ a)

Dentro desta classe de problemas, o segundo problema que nos propusemos resolver e adeterminacao de uma arvore geradora de um grafo. Este problema consiste numa gen-eralizacao do anterior que, em vez de determinar o conjunto (ou lista) dos vertices al-cancaveis de um dado vertice, determina o subconjunto das arestas do grafo necessariaspara alcancar esses vertices desde o vertice dado.Para cumprir esse objectivo basta-nos guardar uma lista com pares de vertices. Ve-jamos por exemplo a generalizacao da travessia depth first.

arvGerD g o = depthF’ g [] [(o,x) | x <- adjacentes g o]

depthF’ g a [] = reverse adepthF’ g a ((o,d):as)| d ‘elem‘ (map snd a) = depthF’ g a as| otherwise =let a’ = [(d,d’) | d’ <- adjacentes g d]in depthF’ g ((o,d):a) (a’ ++ as)

Note-se finalmente que como esta funcao pode ser vista como uma generalizacao daprimeira, podemos redefinir a funcao alcancaveisD a partir desta:

alcancaveisD g o = o:(map snd (arvGerD g o))

Vamo-nos agora debrucar sobre o problema de calcular uma arvore geradora de customınimo.Existem varios algoritmos para resolver este problema. A primeira solucao apresen-tada foi feita por Otakar Bor?vka em 1926.

Page 161: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

15. GRAFOS 157

Um outro algoritmo, apresentado por Joseph Kurskal (em 1956) (e conhecido poralgoritmo de Kurskal) baseia-se no facto de que a arvore geradora de um grafo ligadocom n vertices e um subgrafo ligado e acıclico com exactamente n− 1 arestas.Assim sendo, podemos calcular a arvore geradora de custo mınimo escolhendo as n− 1arestas de menor peso que nao formam qualquer ciclo.De forma a determinar se uma aresta forma um ciclo com um conjunto de outrasarestas vamos manter a informacao sobre quais os vertices que ja se encontram ligadosate ao momento: desta forma nao seleccionaremos uma aresta que ligue dois verticespreviamente ligados.Da descricao acima podemos definir a seguinte funcao:

arvGCMin g =let ar = ordena (arestas g)

vs = vertices gligados = [[v] | v <- vs]

in calculaArvore ((length vs) - 1) ar ligados

ordena (x:y:ys) = insere x (ordena (y:ys))ordena x = x

insere x [] = [x]insere (a,b,c) ((x,y,p):r)| c <= p = (a,b,c):(x,y,p):r| otherwise = (x,y,p):(insere (a,b,c) r)

calculaArvore 0 _ _ = []calculaArvore n [] _ = []calculaArvore n ((x,y,p):ar) lig =case junta x y lig ofJust lig’ -> (x,y,p):(calculaArvore (n-1) ar lig’)Nothing -> calculaArvore n ar lig

Dados dois vertices, a funcao junta verifica se esses vertices ja se encontram ligados.Se tal acontecer, retorna Nothing. No outro caso retorna a nova lista de ligacoesexistentes (juntando as listas correspondentes a cada um dos vertices).

junta v1 v2 (ll:lls)| v1 ‘elem‘ ll =if v2 ‘elem‘ llthen Nothingelse Just (acrescenta ll v2 lls)| v2 ‘elem‘ ll = Just (acrescenta ll v1 lls)

Page 162: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

158 15. GRAFOS

| otherwise =case (junta v1 v2 lls) ofNothing -> NothingJust lls’ -> Just (ll:lls’)

whereacrescenta ll v (x:xs)| v ‘elem‘ x = (ll++x):xs| otherwise = x:(acrescenta ll v xs)

A ultima classe de problemas que referimos foi a dos problemas de todas as origens,todos os destinos.O problema mais simples desta classe consiste em determinar, num dado grafo, aconectividade entre todos os vertices.Para tal podemos pensar numa estrategia que para cada par de vertices, invoca afuncao haCaminho, para testar se tais vertices estao ou nao ligados. Apesar de simples,esta estrategia e muito pouco eficiente.Um outro algoritmo, muito mais eficiente, e conhecido como algoritmo de Warshall,pode ser descrito da seguinte forma.

Considere-se que existe um conjunto - V - de vertices, inicialmentevazio e que inicialmente o grafo G+ tem o mesmo conjunto de ramosde G. Vamos, para cada vertice de G, acrescenta-lo ao conjunto V,mantendo invariantemente verdadeira a seguinte propriedade: Oramo (u,v) existe em G+ se e so se existe em G um caminho comorigem u e destino v e em que todos os nodos intermedios queconstituem esse caminho pertencem a V. Quando o conjunto Vcontiver todos os vertices de G entao esta propriedade transforma-seem: O ramo (u,v) existe em G+ se e so se existe em G um caminhocom origem ue destino v e em que todos os nodos intermedios queconstituem esse caminho pertencem a G.

Esta descricao pode ser codificada na seguinte definicao em Haskell.

warshall g = wrs a v wherea = [(o,d) | (o,d,_) <- arestas g]v = vertices gwrs as [] = aswrs as (x:xs) =letas’ = acrescenta as [(o,d) | o <- os, d <- ds]os = [o | (o,d) <- l, d == x]ds = [d | (o,d) <- l, o == x]

in wrs as’ xs

Page 163: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

15. GRAFOS 159

acrescenta l [] = lacrescenta l ((o,d):ods)| (o,d) ‘elem‘ l = acrescenta l ods| otherwise = acrescenta ((o,d):l) ods

Nas definicoes que apresentamos ate aqui, nunca referimos qual o tipo que vai serusado para representar grafos. Em vez disso, enumeramos algumas operacoes quedevem estar disponıveis sobre tal tipo. Nomeadamente, referimos que devem estardefinidas as seguintes funcoes:

• adjacente que testa se dois vertices sao adjacentes num determinado grafo;• adjacentes que, para um determinado vertices, calcula a lista dos vertices

que lhe sao adjacentes;• aresta que, para um par de vertices calcula (se existir) a aresta entre dois

vertices;• peso que dados dois vertices adjacentes, calcula o peso da aresta entre esses

vertices;• vertices que calcula a lista dos vertices de um grafo;• arestas que calcula a lista das arestas de um grafo.

Esta estrategia e comum e corresponde de certa forma ao uso de classes. A enumeracaoque foi feita acima tem uma traducao quase imediata na seguinte definicao de umaclasse.

class Grafo g whereadjacente :: (Eq v) => g v a -> v -> v -> Booladjacentes :: (Eq v) => g v a -> v -> [v]aresta :: (Eq v) => g v a -> v -> v -> Maybe apeso :: (Eq v) => g v a -> v -> v -> avertices :: (Eq v) => g v a -> [v]arestas :: (Eq v) => g v a -> [(v,v,a)]

adjacente g o d = d ‘elem‘ (adjacentes g o)peso g o d =case (aresta g o d) ofNothing -> undefinedJust x -> x

Vamos finalizar este capıtulo com a apresentacao de duas instancias desta classe.A primeira corresponde a representar um grafo com duas componentes.

• Os vertices do grafo (usando uma lista)• As arestas do grafo (guardando para cada aresta, a origem, o destino e o

peso).

Page 164: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

160 15. GRAFOS

data Padj v a = Padj { vert :: [v], ares :: [(v,v,a)]}

instance Grafo LAdj whereadjacentes g o = [d | (x,d,_) <- ares g, x == o]aresta g o d =case [a | (x,y,a) <- ares g, x == o, y == d] of[] -> Nothing[x] -> Just x

vertices g = vert garestas g = ares g

Apesar de simples, esta implementacao nao e muito eficiente.Alternativamente podemos armazenar, para cada vertice, a lista de todas as arestasque o tem como origem. Esta representacao e conhecida por listas de adjacencia.

data LAdj v a = LAdj { adj :: [(v,[(v,a)])]}

instance Grafo LAdj whereadjacentes g o =case lookup o (adj g) ofNothing -> []Just a -> map fst a

aresta g o d =case lookup o (adj g) ofNothing -> NothingJust a ->case lookup d a ofNothing -> NothingJust x -> Just x

vertices g = map fst (adj g)arestas g =let aresta (x,a) = map (\(y,p) -> (x,y,p)) ain concatMap aresta (adj g)

Note-se finalmente que as definicoes das funcoes que apresentamos acima tem real-mente um tipo diferente do que na altura indicamos.Por exemplo, para a funcao haCaminho da pagina 151, apresentamos o tipo

haCaminho :: (Eq v) => G v a -> v -> v -> Bool

De facto, e baseados na classe definida acima, teremos que o tipo desta funcao e antes

haCaminho :: (Eq v, Grafo g) => g v a -> v -> v -> Bool

Page 165: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

15. GRAFOS 161

Exercıcios

15.1 Modifique a definicao da funcao warshall da pagina 158 de forma a calcularos pesos dos caminhos mais curtos entre cada par de vertices. Adicionalmentepodera ainda guardar a sequencia de vertices que compoem esses caminhos.

Page 166: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao
Page 167: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

CAPITULO 16

Reconhecimento de texto

No Capıtulo 10 referimos a classe Read como a classe dos tipos para os quais existe umafuncao de leitura – read – que converte Strings em elementos desse tipo. Na alturaapenas referimos que esta e uma das classes que sao derivaveis mas nao apresentamosnenhuma definicao de instancia desta classe.O exemplo que vamos usar para exemplificar este problema e o de expressoes ar-itmeticas simples. Uma definicao possıvel para este tipo e

data Exp= C Int| Mais Exp Exp| Menos Exp Exp| Vezes Exp Exp| Div Exp Expderiving Show

Assim, por exemplo a expressao 2 ∗ (3− ((4 + 5)/3)) e representada por Vezes (C 2)(Menos (C 3) (Div (Mais (C 4) (C 5)) (C 3))).Dito de outra forma, a funcao de leitura (ou de reconhecimento) da string "2 * (3 -((4 + 5) / 3))" deve dar como resultado o elemento Vezes (C 2) (Menos (C 3)(Div (Mais (C 4) (C 5)) (C 3))) do tipo Exp.Neste caso o reconhecimento e nao ambıguo. Tal nao e o caso em geral. Veja-se porexemplo a string "2-3+4". Aqui o reconhecimento poderia produzir

• Menos (C 2) (Mais (C 3) (C 4)) ou• Mais (Menos (C 2) (C 3)) (C 4).

De uma forma geral, o problema de reconhecimento passa por duas etapas:

(1) Em primeiro lugar ha que separar a string de entrada em varios blocos,correspondendo cada um destes a um sımbolo. Por exemplo na string "33 +456" podemos identificar 3 desses blocos: "33" "+" e "345".

(2) Em segundo lugar deve-se construir o valor correspondente a partir dos val-ores de cada um destes blocos.

163

Page 168: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

164 16. RECONHECIMENTO DE TEXTO

Uma forma alternativa para abordar este problema e considerar que as funcoes dereconhecimento de um elemento do tipo a recebem como argumento uma String edao como resultado um par do tipo (a,String) sendo

• a primeira componente o valor correspondente• a segunda componente o prefixo da string original que nao foi processada.

Esta forma de fazer o reconhecimento, em que se vao encadeando accoes que por suavez vao construindo o input para a accao seguinte e conhecido por reconhecimentobaseado em continuacoes.Uma generalizacao deste conceito e, cada uma das funcoes fornecer, em vez de um par,uma lista de pares, correspondentes a varias possibilidades de uso da string inicial. Aabreviatura de tipos ReadS define-se como

type ReadS a = String -> [(a,String)]

Uma funcao f :: ReadS a e uma funcao que, dada uma string s, retorna uma listade pares (x,s’) em que

• x e um elemento do tipo a e• s’ e um prefixo de s.

As expressoes do tipo ReadS a chamaremos reconhecedores de elementos do tipo a.Por exemplo, para o tipo Exp definido acima, uma funcao de conversao readExp ::ReadS Exp, para o argumento 3 + 5 ∗ 2, poderia dar como resultado a lista

[((C 3), "+ 5 * 2"),

((Mais (C 3) (C 5)), "* 2"),

((Mais (C 3) (Vezes (C 5) (C 2))), ""),

((Vezes (Mais (C 3) (C 5)) (C 2)), "")]

Na base de todas as funcoes de conversao esta a funcao lex que, dada uma string s,retorna uma lista de pares (w,s’) em que

• w e a primeira palavra de s e• s’ e o resto de s.

Eis alguns exemplos.Prelude> lex "a funcao lex"

[("a"," funcao lex")]

Prelude> lex "a, funcao lex"

[("a",", funcao lex")]

Prelude> lex ", a funcao lex"

[(","," a funcao lex")]

Prelude> lex "234+-3"

[("234","+-3")]

Prelude> lex "+-3"

[("+-","3")]

Prelude> lex "-+3"

[("-+","3")]

Page 169: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

16. RECONHECIMENTO DE TEXTO 165

Prelude> lex "-(-3)"

[("-","(-3)")]

Prelude> lex "-3"

[("-","3")]

Na verdade, esta funcao retorna sempre uma lista com um unico par. O uso destetipo tem como vantagem a uniformizacao e, por isso mesmo, uma mais facil integracaodesta funcao no problema.Vamos comecar por apresentar uma serie de funcoes auxiliares que nos permitiraoestruturar as nossas definicoes.Em primeiro lugar vamos definir uma funcao (de ordem superior) que, dadas duasfuncoes de conversao para tipos a e b, constroi a funcao de conversao de um elementodo tipo a seguido de um elemento do tipo b.

depois :: (ReadS a) -> (ReadS b) -> ReadS (a,b)depois _ _ [] = []depois r1 r2 input =[((x,y),i2) | (x,i1) <- r1 input, (y,i2) <- r2 i1]

Vejamos um exemplo de utilizacao desta funcao:Main> lex "23 34 45"

[("23"," 34 45")]

Main> lex " 34 45"

[("34"," 45")]

Main> depois lex lex "23 34 45"

[(("23","34")," 45")]

Usando a facilidade de transformar um operador pre-fixo (depois) em infixo (‘depois‘),poderıamos ter escrito a ultima expressao como:

Main> (lex ‘depois‘ lex) "23 34 45"

[(("23","34")," 45")]

Main>

Uma outra forma de combinar estas funcoes de conversao e pela uniao de funcoes;esta uniao pode ser feita de duas formas.

• Invocando as duas funcoes de conversao independentemente dos seus resul-tados.

ou :: (ReadS a) -> (ReadS a) -> ReadS aou r1 r2 input = (r1 input) ++ (r2 input)

• Invocando apenas a segunda das funcoes no caso da primeira nao ter obtidoqualquer valor.

senao :: (ReadS a) -> (ReadS a) -> ReadS asenao r1 r2 input =

Page 170: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

166 16. RECONHECIMENTO DE TEXTO

case (r1 input) of[] -> r2 inputl -> l

Usando estas funcoes, podemos construir uma funcao que, dada uma funcao de con-versao para um dado tipo a, constroi a funcao que trata o caso da string inicial estarenglobada por parentesis.

pCurvos = parentesis "(" ")"

parentesis :: Char -> Char -> (ReadS a) -> ReadS aparentesis _ _ _ [] = []parentesis ap pa r input =[(x,c) |((_,(x,_)),c)<- ((readC ap) ‘depois‘ (r ‘depois‘ (readC pa))) input]

readC :: String -> ReadS StringreadC c = (filter ((== c).fst)) . lex

Podemos re-utilizar muita desta definicao para tratar o caso de outros tipos de parentesis.

pRectos = parentesis ’[’ ’]’pChavetas = parentesis ’{’ ’}’

Finalmente vamos definir uma funcao para converter uma serie de elementos de umtipo.

readSeq :: (ReadS a) -> ReadS [a]readSeq r input =case (r input) of[] -> [([],input)]l -> concat (map continua l)wherecontinua (a,i) = map (c a) (readSeq r i)c x (xs,i) = ((x:xs),i)

Com estas funcoes vamos definir a funcao de conversao de strings para o tipo Expdefinido na seccao anterior.Para convertermos o caso mais simples de expressoes (numeros inteiros), vamos usara funcao pre-definida para os inteiros (que como sao uma instancia da classe Read,podemos usar a funcao reads).

Page 171: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

16. RECONHECIMENTO DE TEXTO 167

readNum :: ReadS ExpreadNum input = [(C x,y) | (x,y) <- reads input]

Esta mesma definicao pode ser escrita como sem usar listas por compreensao.

readNum :: ReadS ExpreadNum = (map (\ (x,y) -> ((C x), y))) . reads

Para convertermos expressoes que involvam operadores binarios vamos usar a seguintefuncao.

readBinOp =readMais ‘senao‘ readMenos ‘senao‘readVezes ‘senao‘ readDiv

readMais = (organiza Mais) . (readOp "+")readMenos = (organiza Menos) . (readOp "-")readVezes = (organiza Vezes) . (readOp "*")readDiv = (organiza Div) . (readOp "/")

readOp s =(readNum ‘ou‘ (pcurvos readExp))‘depois‘ (readC s)‘depois‘ readExp

organiza f l = [(f a b, s) | ((a,_,b),s) <- l]

A partir destas funcoes, podemos agora definir a funcao que converte strings em termosdo tipo Exp.

readExp :: ReadS ExpreadExp = readBinOp ‘ou‘ readNum ‘ou‘ (pcurvos readExp)

Existe na definicao de readOp uma subtileza. Se seguıssemos a definicao do tipo dedados Exp serıamos tentados a definir a funcao readOp como

readOp s = readExp ‘depois‘ (readC s) ‘depois‘ readExp

Com esta definicao a definicao de readExp contem um ciclo infinito. Vejamos porque.

(1) A definicao de readExp comeca por invocar a funcao readBinOp(2) Esta, sem consumir nenhum input invoca a funcao readMais(3) Esta invoca a funcao readOp "+"(4) Segundo esta ultima definicao, a funcao readOp comeca por invocar a funcao

readExp.

Page 172: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

168 16. RECONHECIMENTO DE TEXTO

Temos por isso que a funcao readExp invoca-se a si propria sem ter consumido qual-quer parte do input. Daı que isto resulte num ciclo infinito. Com a solucao queapresentamos em primeiro lugar, este ciclo nao existe pois so se da uma nova in-vocacao de readExp depois de ter sido consumido algum input – no mınimo o sinalde parentesis.Podemos agora definir o tipo Exp como uma instancia da classe Read:

instance Read Exp wherereadsPrec _ = readExp

Vejamos entao o comportamento das funcoes que acabamos de definir.Main> readExp "23 + 34"

[(Mais (C 23) (C 34),""),(C 23," + 34")]

Main> readExp "(23 + 34)"

[(Mais (C 23) (C 34),"")]

No primeiro calculo, existem duas formas de obter uma expressao a partir da string"23+34".

• A expressao Num 23, nao usando a parte "+34" ou• A expressao Bin (Num 23) Mais (Num 34) usando toda a string.

No segundo calculo, ja so existe uma forma de fazer a conversao.Se alterarmos a definicao da funcao readExp para

readExp :: ReadS ExpreadExp’ =readBinOp ‘senao‘ (readNum ‘senao‘ (pcurvos readExp))

podemos ver que os resultados do calculo anterior ja sao diferentes.Main> readExp’ "23 + 34"

[(Mais (C 23) (C 34),"")]

Main> readExp’ "(23 + 34)"

[(Mais (C 23) (C 34),"")]

Isto porque a funcao senao so vai invocar o seu segundo argumento se a invocacao doprimeiro nao tiver tido sucesso.Para finalizar esta seccao, vamos apresentar uma funcao que, dada uma string querepresenta uma expressao escrita em notacao infixa, a transforma em pos-fixa.

postfix :: String -> Stringpostfix = pfix . read

pfix (C x) = (show x) ++ " "

Page 173: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

16. RECONHECIMENTO DE TEXTO 169

pfix (Mais e1 e2) = (pfix e1) ++ (pfix e2) ++ "+ "pfix (Menos e1 e2) = (pfix e1) ++ (pfix e2) ++ "- "pfix (Vezes e1 e2) = (pfix e1) ++ (pfix e2) ++ "* "pfix (Div e1 e2) = (pfix e1) ++ (pfix e2) ++ "/ "

Exercıcios

16.1 A funcao readExp definida acima nao tem em conta as usuais precedencias dosvarios operadores.(1) Qual o resultado de readExp "23 * 5 - 2"?(2) Modifique as definicoes acima, de forma a que readExp "23 * 5 - 2"

tenha como resultadoMenos (Vezes (C 23) (C 5)) (C 2)

Page 174: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao
Page 175: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

CAPITULO 17

Computacao com estado

Suponhamos que queremos definir uma funcao de etiquetagem de arvores binarias deacordo com uma determinada estrategia de travessia.Veja-se por exemplo as seguintes arvores:

b

d

g h

a

c

fe

(2,b)

(3,d)

(4,g) (5,h)

(1,a)

(6,c)

(8,f)(7,e)

(1,b)

(3,d)

(2,g) (4,h)

(5,a)

(7,c)

(8,f)(6,e)

As duas arvores em baixo correspondem a etiquetar a arvore de cima. Destas, a daesquerda corresponde a uma estrategia inorder, e a da direita corresponde a umaestrategia preorder.Comecemos entao por resolver este problema para o caso da estrategia inorder.Aquilo que queremos e definir uma funcao etiqueta com o seguinte tipo

171

Page 176: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

172 17. COMPUTACAO COM ESTADO

etiqueta :: Btree a -> Btree (a, Int)

Uma primeira abordagem a este problema consiste em definir tal funcao de uma formaestrutural, i.e., guiados pela estrutura do argumento. O esqueleto de tal funcao e

etiqueta Vazia = ...etiqueta (Nodo r e d) =let e’ = etiqueta e

d’ = etiqueta dl = ...

in Nodo (r,l) e’ ...

A primeira linha e facil de preencher: a etiquetagem de uma arvore vazia produz umaarvore vazia. Os problemas surgem quando queremos calcular, quer a etiqueta da raız,quer as etiquetas da sub-arvore da direita: estas dependem do numero de nodos dasub-arvore esquerda.Precisamos para isso de, em primeiro lugar, uma funcao de contagem de nodos.

contaNodos :: Btree a -> IntcontaNodos Vazia = 0contaNodos (Nodo _ e d) = (contaNodos e) + 1 + (contaNodos d)

Em segundo lugar, precisamos de uma funcao que some um determinado numero asetiquetas de uma arvore. Podemos desde ja pensar que tal funcao pode ser definidade uma forma mais generica, usando uma generalizacao da funcao map das listas.

mapBtree :: (a -> b) -> Btree a -> Btree bmapBtree _ Vazia = VaziamapBtree f (Nodo r e d) = Nodo (f r) (mapBtree f e) (mapBtree f d)

aumenta n a = mapBtree (\ (x,y) -> (x,n + y) ) a

Estamos agora em condicoes de definir a funcao pretendida.

etiqueta Vazia = Vaziaetiqueta (Nodo r e d) =let e’ = etiqueta e

d’ = etiqueta dl = contaNodos e

in Nodo (r,l) e’ (aumenta (l+1) d’)

Esta definicao resulta numa funcao muito pouco eficiente:

Page 177: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

17. COMPUTACAO COM ESTADO 173

• Em primeiro lugar, e depois de percorrer a sub-arvore da esquerda para aetiquetar, temos que voltar a percorre-la para calcular o numero de nodos.• Em segundo lugar, no que respeita a etiquetagem da sub-arvore da direita,

depois de efectuarmos esta tarefa temos que voltar a percorrer a dita arvorepara actualizarmos as suas etiquetas.

O segundo problema pode ser resolvido recorrendo a estrategia do uso de acumuladoresvista no Capıtulo 3. Vamos de finir uma funcao auxiliar – etiquetaDe – que, alemde receber a arvore a etiquetar como argumento, recebe o valor da primeira etiquetaa ser usada.

etiqueta a = etiquetaDe a 1etiquetaDe Vazia _ = VaziaetiquetaDe (Nodo r e d) n =let e’ = etiquetaDe e n

l = n + (contaNodos e)d’ = etiquetaDe d (l + 1)

in Nodo (r,l) e’ d’

Esta solucao continua a sofrer do primeiro problema assinalado acima.Para resolver este problema podemos generalizar a estrategia anterior e alem deaceitarmos um parametro adicional com o valor da etiqueta a ser usada, passamosainda, no resultado a produzir o valor da proxima etiqueta a ser usada.

etiquetaDeA :: Btree a -> Int -> (Int, Btree (Int,a))etiquetaDeA Vazia x = (Vazia, x)etiquetaDeA (Nodo r e d) x =let (x’, e’) = etiquetaDeA e x

x’’ = x’ + 1(x’’’, d’) = etiquetaDeA d x’’

in (x’’’, Nodo (r,x’) e’ d’)

A funcao etiqueta, que invoca esta, tem agora de fornecer como primeira etiqueta ovalor de 1 e no final seleccionar a parte do resultado que interessa.

etiqueta a = snd (etiquetaDeA a 1)

Uma das razoes pela qual as tres definicoes apresentadas tem uma eficiente crescente eo detalhe crescente com que a ordem pela qual os varios passos sao efectuados. Senaovejamos.

• Na primeira definicao, a unica imposicao que e feita e que a actualizacao dasetiquetas da sub-arvore direita so pode ser feita depois de contarmos quantosnodos existem na sub-arvore esquerda.

Page 178: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

174 17. COMPUTACAO COM ESTADO

• Na segunda definicao, a etiquetagem da sub-arvore direita so pode ser feitadepois de contarmos quantos nodos existem na sub-arvore esquerda.• Na ultima definicao, a etiquetagem da sub-arvore direita so pode ser feita

depois etiquetarmos a sub-arvore esquerda.

Esta sequenciacao de tarefas e conseguida atarves do uso de parametros e/ou resulta-dos adicionais que sao transmitidos de uma tarefa para a seguinte.Atentemos agora na ultima definicao apresentada. Repare-se, em particular, na mu-danca de tipo que foi efectuada, da solucao inicial para esta. Comecamos com umafuncao de tipo

Btree a -> Btree (Int, a)

e acabamos por usar uma funcao de tipo

Btree a -> Int -> (Int, Btree (Int, a)

De uma forma mais geral, em vez de usarmos um determinado tipo b estamos a usaro tipo a -> (a,b). Mais ainda, este tipo a corresponde a informacao que vamosguardando de umas invocacoes para as outras, e que nos permite forcar uma deter-minada ordem de execucao das varias tarefas. E costume chamar a esta informacao oestado das computacoes. Dizemos entao que o tipo a -> (a,b) e um transformadorde estados (State Transformer na literatura anglo-saxonica).Por razoes que se prendem com a implementacao da linguagem Haskell (todos ostipos de dados definidos com uma clausula data devem ter um construtor associado),vamos definir o seguinte tipo.

data ST s v = St (s -> (s,v))

Como veremos mais a frente, o codigo que vamos apresentar fica mais legıvel se usarmosantes uma definicao com selectores explıcitos.

data ST s v = St { transf :: s -> (s,v)}

Relembremos que esta definicao corresponde, em grande parte, ao seguinte conjuntode definicoes.

data ST s v = St (s -> (s,v))

transf :: ST s v -> (s -> (s,v))transf (St f) = f

A execucao de um destes transformadores de estados consiste em fornecer-lhe umestado inicial e, no final, seleccionar o valor calculado.

run :: ST s v -> s -> vrun p s = snd (transf p s)

Page 179: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

17. COMPUTACAO COM ESTADO 175

Vejamos agora algumas formas de construir e combinar transformadores de estado.Um dos mais simples transformadores de estados e aquele que nao transforma o estado.

return :: a -> ST s areturn x = St \s -> (s, x)

Note-se que esta funcao, recebe como argumento o valor que deve ser calculado a saıdado transformador de estados.Uma outra forma de construir transformadores de estados, consiste em fornecer afuncao de transformacao de estado, ignorando qual o valor calculado:

mudaEstado :: (s->s) -> ST s ()mudaEstado f = St (\s -> (f s, ()))

No que respeita a sequenciacao de transformadores de estados, temos duas alternativas.

• depois1 :: (ST s v1) -> (ST s v2) -> (ST s v2)depois1 t1 t2 =ST(\s ->let (s’ ,v1) = transf t1 s

(s’’,v2) = transf t2 s’in (s’’,v2))

• depois2 :: (ST s v1) -> (v1 -> (ST s v2) -> (ST s v2)depois2 t f =ST(\s ->let (s’ ,v1) = transf t s

(s’’,v2) = transf (f v1) s’in (s’’,v2))

Note-se que na primeira destas alternativas o valor calculado e simplesmente ignorado.Ja na segunda alternativa, este valor e usado para calcular qual o transformador deestados a ser usado. De facto, a primeira das alternativas e uma variante da segunda:

depois1 t1 t2 = depois2 t1 (\x -> t2)

Page 180: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

176 17. COMPUTACAO COM ESTADO

Vejamos agora como a ultima solucao apresentada para o problema da etiquetagemde uma arvore binaria pode ser expressa com estes combinadores.

etiqueta a = run (etiquetaST a) 1

etiquetaST Vazia = return VaziaetiquetaST (Nodo r e d) =etiquetaST e ‘depois2‘ (\ e’ ->estado? ‘depois2‘ (\ l ->mudaEstado succ ‘depois1‘etiquetaST d ‘depois2‘ (\ d’ ->return (Nodo (l,r) e’ d’))))

O problema de impor uma determinada ordem na execucao de varias tarefas ja foimencionado no Capıtulo 11.Nesse contexto referimos que a solucao usada em Haskell e baseada em monadesdos quais o tipo IO e um caso especial (ver pagina 117).De facto as monades sao definidas em Haskell como uma classe de construtores detipos.

class Monad m wherereturn :: a -> m a(>>=) :: m a -> (a -> m b) -> m b(>>) :: m a -> m b -> m b

x >> y = x >>= (\ _ -> y)

Aquilo que apresentamos nessa altura foram as correspondentes instancias para oconstrutor de tipos IO.A motivacao que demos para as funcoes de composicao de computacoes (>>= e >>)sao exactamente as mesmas que usamos para definir as funcoes depois2 e depois1.Podemos por isso apresentar as definicoes acima dentro deste contexto, e como adefinicao de mais uma instancia da dita classe.

instance Monad (ST s) wherereturn x = ST (\s -> (s,x))p >>= f =ST(\s ->let (s’ ,v1) = transf t s

(s’’,v2) = transf (f v1) s’

Page 181: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

17. COMPUTACAO COM ESTADO 177

in (s’’,v2))

A vantagem de definirmos as operacoes de sequenciacao desta forma e podermos usartoda a terminologia e definicoes standard sobre as monades. Nomeadamente, podemosusar a notacao do para escrever a funcao etiqueta.

etiqueta a = run (etiquetaST a) 1

etiquetaST Vazia = return VaziaetiquetaST (Nodo r e d) = do{ e’ <- etiquetaST e; l <- estado?; mudaEstado succ; d’ <- etiquetaST d; return (Nodo (l,r) e’ d’)}

O uso de monades em geral, e em particular desta instancia dos transformadores deestados, e uma solucao muito elegante para definir, no paradigma da programacao fun-cional, solucoes que passam pela manutencao de um conjunto de informacao de umascomputacoes para outras. Este mesmo efeito e obtido noutras linguagens recorrendoao uso (por vezes abusivo) de variaveis globais.Vamos terminar este capıtulo apresentando solucoes deste tipo para dois problemasque vimos em capıtulos anteriores:

• o reconhecimento de texto visto no Capıtulo 16;• a determinacao de um caminho entre dois vertices de um grafo.

Quando falamos no reconhecimento de texto (Capıtulo 16), aquilo que pretendıamosera definir uma funcao readExp que, recebendo uma string devolvesse a correspondenteexpressao do seguinte tipo Exp:

data Exp = C Int| Mais Exp Exp| Menos Exp Exp| Vezes Exp Exp| Div Exp Expderiving Show

Neste problema existe uma dificuldade acrescida: o resultado da funcao readExp devepoder indicar que o reconhecimento nao foi possıvel.Podemos resolver este pormenor de duas maneiras distintas:

Page 182: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

178 17. COMPUTACAO COM ESTADO

• usando o tipo ST acima em que os valores retornados serao do tipo MaybeExp• usando a seguinte variante do tipo ST:

data STMaybe s v = STMaybe { trans :: s -> Maybe (s,v)}

A vantagem de usarmos esta segunda alternativa e podermos incluir na definicao dacomposicao sequencial (i.e., na definicao da operacao >>=) o facto de que a composicaode uma computacao sem sucesso com outra qualquer, produz uma computacao semsucesso.Vejamos entao, e em primeiro lugar, como e que se define esta instancia particular daclasse Monad.

instance Monad (STMaybe s) wherereturn x = STMaybe (\s -> Just (s,x))p >>= f =STMaybe(\s ->case trans p s ofNothing -> NothingJust (s’, x’) -> trans (f x’) s’

)

Neste problema, o estado vai ser constituıdo pela parte do input que ainda nao foiconsumida.

type Parser a = STMaybe String a

Vejamos entao como implementar os combinadores definidos no Capıtulo 16.

lexM :: Parser StringlexM = STMaybe (Just . head . swap . lex)where swap (x,y) = (y,x)

erroM :: Parser aerroM = STMaybe (\s -> Nothing)

senao :: (Parser a) -> (Parser a) -> (Parser a)senao p q =STMaybe(\s ->case trans p s of

Page 183: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

17. COMPUTACAO COM ESTADO 179

Nothing -> trans q sJust x -> Just x

)

readChar :: Char -> Parser CharreadChar c =do {x <- lexM ;if x == [c]then return celse erroM}

parentesis :: Char -> Char -> Parser a -> Parser aparentesis a f p =do {readChar a ;x <- p ;readChar f ;return x}

readSeq :: Parser a -> Parser [a]readSeq p =do {x <- p ;xs <- readSeq p ;return (x:xs)} ‘senao‘ (return [])

Vejamos entao como a definicao da funcao de reconhecimento de uma expressao podeser definida:

parseExp :: Parser ExpparseExp = parseBinOp‘senao‘ parseNum‘senao‘ (parentesis ’(’ ’)’ parseExp)

parseBinOp = parseMais‘senao‘ parseMenos‘senao‘ parseVezes‘senao‘ parseDiv

Page 184: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

180 17. COMPUTACAO COM ESTADO

parseMais = parseBOP Mais ’+’parseMenos = parseBOP Menos ’-’parseVezes = parseBOP Vezes ’*’parseDiv = parseBOP Div ’/’

parseBOP x y = do{ op1 <- parseOp; readChar y; op2 <- parseExp; return (x op1 op2)}

parseOp = (parentesis ’(’ ’)’ parseExp) ‘senao‘ parseNum

parseNum = do{ n <- lexM; case reads n of[] -> erroM((x,_):_) -> return (C x)

}

A invocacao desta computacao, tal como acontecia com a etiquetagem das arvores,corresponde a fornecer o estado inicial (a string a ser analisada) e, no final seleccionarapenas o valor da expressao calculada.

runSTMaybe :: (STMaybe s v) -> s -> Maybe vrunSTMaybe p s =case trans p s ofJust (_,v) -> Just vNothing -> Nothing

readExp :: String -> Maybe ExpreadExp s = runSTMaybe parseExp s

No Capıtulo 15 apresentamos a seguinte solucao para o calculo de um caminho entredois vertices de um grafo.

caminho :: (Eq v) => G v a -> v -> v -> Maybe [v]caminho g o d = caminhoV [] g o d wherecaminhoV v g o d| adjacente g o d = Just [o,d]| otherwise =

Page 185: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

17. COMPUTACAO COM ESTADO 181

case (umCaminho (o:v) g (adjacentes g o) d) ofNothing -> Nothing(Just c) -> Just (o:c)

umCaminho v g [] y = NothingumCaminho v g (x:xx) y| x ‘elem‘ v = umCaminho v g xx y| otherwise =case (caminhoV v g x y) ofNothing -> umCaminho v g xx y(Just c) -> Just c

A funcao usa uma funcao auxiliar com um parametro adicional – a lista dos vertices javisitados. Neste caso esta lista e apenas usada para evitar entrar em ciclos. Podemosno entanto usar esta lista como a lista dos vertices para os quais ja foi tentado encontraro caminho e que por isso nao devem ser tentados outra vez. Para isso seria necessarioque o valor desta lista fosse passado de umas invocacoes para as outras.Podemos por isso imaginar esta estrategia usando um transformador de um estadoque e esta lista dos vertices visitados.Tambem aqui temos uma funcao que pode ou nao ter sucesso pelo que vamos usar otipo STMaybe definido para o problema do reconhecimento de texto.

caminhoST :: (Eq v) => G v a -> v -> v -> Maybe [v]caminhoST g o d = runSTMaybe (camST g o d) [] wherecamST g o d| adjacente g o d = return [o,d]| otherwise = do{ visita o; c <- umCam g (adjacentes g o) d; return (o:c)}

umCam _ [] _ = erroMumCam g (x:xx) y = do{ v <- visitados; if x ‘elem‘ vthen umCam g xx yelse camST g x y

}visita x = STMaybe (\s -> Just (x:s,()))visitados = STMaybe (\s -> Just (s,s))

Vejamos um exemplo de um grafo em que esta ultima solucao se pode mostrar maiseficiente.

Page 186: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

182 17. COMPUTACAO COM ESTADO

A

B

C

D

E

G

F

H

K

J

I

Suponhamos que procuramos no grafo acima um caminho entre os nodos D e J; supon-hamos ainda que, ao calcularmos os vertices adjacentes a D, obtemos a lista [B,G,I].Na primeira solucao apresentada, irıamos por isso comecar por procurar o vertice J apartir dos alcancaveis de B, visitando todos os vertices A,C,F,H,K,E e nao encontrandoo vertice em causa. Continuarıamos com os alcancaveis de G, visitando de novo osvertices B,A,C,F,H,K,E. So quando tentarmos o terceiro vertice e que encontraremosa solucao.Ao usar a segunda solucao, a visita aos alcancaveis de G resume-se a constatar quetodos os seus adjacentes ja foram tentados, pelo que nao voltam a ser visitados.

Exercıcios

17.1 Uma variante do problema de etiquetagem dos nodos de uma arvore com umnumero inteiro e a etiquetagem com os elementos sucessivos de uma dada lista.Apresente uma solucao para este problema, adaptando a solucao que foi propostana pagina 173.

17.2 Redefina as funcoes apresentadas no Capıtulo 15 usando, sempre que possıvel,um estado para optimizar as computacoes.

Page 187: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

Glossario

., 66

/=, 109

==, 109

=>, 107

>>, 117, 176

>>=, 117, 176

arvore, 129

balanceadas, 132

binaria, 97

de procura, 130, 144

de um termo, 16

geradora de um grafo, 156

irregular, 99

ghci, 4

hugs, 4

abstraccao, 83

acumulador, 37, 77, 151, 155, 173

acumuladore, 49

algoritmo

de Eratostenes, 71

de Warshall, 158

all, 79

and, 79

any, 79

appendFile, 122

arco de um grafo, 150

Bool, 95

breadthF, 155

caminho, 152

caminhoMCurto, 152

ciclo infinito, 168

classe, 106

de construtores, 112

instancia derivada, 112

sub-classe, 111

comentario, 85

compilador, 121

complexidade

exponencial, 36

linear, 35

quadratica, 56

composicao, 66

computacao, 117

concat, 47

conjunto, 84, 113

construtor de tipo, 46, 98

continuacao, 164

contractum, 19

curry, 8

curry, 65, 66

depthF, 155

divide and conquer, 57

drop, 80

dropWhile, 80

Eq, 109

equacao, 15

especificacao, 15

estado, 174

estrategia de reducao, 19

Euler, 150

exportacao, 126

factorial, 31

fibonnacci, 34

FilePath, 122

filter, 70, 145

flip, 66

fmap, 112

foldl, 76

foldl1, 77

foldr, 75, 101

foldr1, 77

funcao

de ordem superior, 65

funcao finita, 143

183

Page 188: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

184 GLOSSARIO

Functor, 112

getChar, 117

getContents, 120getLine, 119

ghc, 121

grafo, 149, 180

haCaminho, 151

hClose, 123hFileSize, 123

hGetChar, 123

hGetContents, 123hGetLine, 123

hPutChar, 123

hPutStr, 123hPutStrLn, 123

id, 105if then else, 25

implementacao, 84

import, 126importacao, 126

inducao natural, 32

indutivadefinicao, 46

infixo, 6

instancia, 18, 106interaccao, 115

interact, 120

invariante, 84

Just, 96

lazy evaluation, 19length, 47

let ... in ..., 23lex, 164

lista, 41

compreensao, 43, 70lookup, 96, 129

modulo, 10, 125monade, 117, 176Main, 121main, 121map, 69, 172

maximum, 77

Maybe, 96minimum, 77

modelo, 83Monad, 176multi-grafo, 150

not, 95

notacao λ, 8, 31

notacao do, 119, 177

Nothing, 96

Num, 106

openFile, 123

or, 79

Ord, 110

ordenacao, 55

insercao ordenada, 56

merge sort, 57

quicksort, 60

Ordering, 110

padrao

exclusivo, 49, 58

pattern-matching, 23

polinomio, 87, 113

prefixo, 6

preorder, 98

primrec, 101

programacao, 15

declarativa, 1

imperativa, 1

putChar, 117

Read, 115, 163

read, 115, 163

readFile, 122

ReadS, 164

reconhecedor, 164

reconhecimento, 177

recursiva

funcao, 31

recursividade primitiva, 32, 76

redex, 19

reducao, 11, 15

renomeacao, 127

return, 117, 176

reverse, 47, 50

scanl, 78

scanl1, 78

scanr, 78

scanr1, 78

selector, 99

Show, 115

show, 115

span, 80

splitAt, 80

stdin, 122

Page 189: Introdu˘c~ao a Programa˘c~ao Funcional · hugs (Haskell Users Gofer System) que tem algumas vantagens do ponto de vista did atico. Existe uma vasta literatura sobre estas implementa˘c~ao

GLOSSARIO 185

stdout, 122string, 41

substituicao, 17

take, 80

takeWhile, 80

tipo, 5construtor de, 7

enumerado, 95

funcao, 7parametrico, 8, 98, 106

polimorfico, 105

polimorfismo, 8primitivo, 5

produto, 7recursivo, 96

soma, 93

variavel de, 8tipo abstracto de dados, 127

todosOsCaminhos, 153

travessiabreadth-first, 154

depth-first, 154

inorder, 171preorder, 171

uncurry, 8uncurry, 65, 66

vertice de um grafo, 150visibilidade, 125

where, 24

writeFile, 122

zip, 48, 79

zipWith, 79