lf apostila

163
PROGRAMAC ¸ ˜ AO FUNCIONAL USANDO HASKELL

Upload: api-3831436

Post on 07-Jun-2015

483 views

Category:

Documents


3 download

TRANSCRIPT

Page 1: LF Apostila

PROGRAMACAO FUNCIONAL

USANDO HASKELL

Page 2: LF Apostila

Programacao FuncionalUsando Haskell

Francisco Vieira de Souza

• Licenciado em Matematica (1978) e Engenheiro Civil (1982) pela Universidade Federaldo Piauı, Mestre (1994) e Doutor (2000) em Ciencia da Computacao pela UniversidadeFederal de Pernambuco.

• Professor do Departamento de Matematica (1986-1988), fundador (1987) e professor (desde1987) do Departamento de Informatica e Estatıstica da Universidade Federal do Piauı.

UFPI/CCN/DIETeresina-Pi

Agosto de 2005

Page 3: LF Apostila

Copyright c©2005, Departamento de Informatica e Estatıstica,Centro de Ciencias da Natureza,Universidade Federal do Piauı.Todos os direitos reservados.

A reproducao do todo ou parte destetrabalho somente sera permitida para finseducacionais e de pesquisa e com a expressaautorizacao do autor.

Copyright c©2005, Departamento de Informatica e Estatıstica,Centro de Ciencias da Natureza,Universidade Federal do Piauı.All rights reserved.

Reproduction of all or part of this work onlywill be permitted for educational or research useand with expressed permission of the author.

iii

Page 4: LF Apostila

Apresentacao

Esta Apostila representa a compilacao de varios topicos, desenvolvidos por pesquisadores derenome, no campo da programacao funcional. Ela tem como objetivo servir de guia aos profis-sionais de Informatica que desejam ter um conhecimento inicial sobre o paradigma funcional deforma geral e, em particular, sobre programacao usando Haskell. Ultimamente, Haskell tem setornado a linguagem funcional padrao do discurso, ja existindo varios interpretadores e compi-ladores para ela, alem de varias ferramentas de analise de programas nela codificados (profiles).

Para atingir este objetivo, acreditamos que o estudo deva ser acompanhado de algum co-nhecimento, mesmo que mınimo, sobre a fundamentacao destas linguagens e da forma comoelas sao implementadas. Este conhecimento proporciona ao leitor uma visao das principais ca-racterısticas e propriedades destas linguagens. Em particular, e importante entender porque astecnicas utilizadas na compilacao das linguagens imperativas nao se mostraram adequadas nacompilacao de linguagens funcionais.

Em 1978, John Backus advogou o paradigma funcional como o que oferecia a melhor solucaopara a chamada “crise do software”. As linguagens funcionais sao apenas uma sintaxe maiscomoda para o λ-calculo. David Turner [36] mostrou, em 1979, que a logica combinatorialpoderia ser extendida de forma a possibilitar a implementacao eficiente de linguagens funcionais.Esse trabalho provocou uma corrida em direcao a pesquisa nesta area, gerando uma variedadede tecnicas de implementacao destas linguagens.

Dentre estas tecnicas, uma que tem sido adotada, com resultados promissores, e a utilizacaodo λ-calculo como linguagem intermediaria entre a linguagem de alto nıvel e a linguagem demaquina. Os programas codificados em alguma linguagem funcional de alto nıvel sao traduzidospara programas em λ-calculo e destes para programas em linguagem de maquina. Neste caso, oλ-calculo desempenha um papel semelhante ao que a linguagem Assembly exerce, como linguagemde montagem, na compilacao de linguagens imperativas. Esta metodologia tem dado certo, umavez que ja se conhecem tecnicas eficientes de traducao de programas em λ-calculo para programasexecutaveis, faltando apenas uma traducao eficiente de programas codificados em uma linguagemfuncional de alto nıvel para programas em λ-calculo.

Esta Apostila tem inıcio em seu primeiro Capıtulo tratando das caracterısticas das lingua-gens funcionais, destacando suas vantagens em relacao as linguagens de outros paradigmas queutilizam atribuicoes destrutivas. Em seguida, e feita uma introducao ao λ-calculo. Apesar docarater introdutorio, achamos ser suficiente para quem quer dar os primeiros passos em direcaoa aprendizagem desta tecnica. Os Capıtulos subsequentes se referem todos a Programacao Fun-cional usando Haskell.

Por ser uma primeira tentativa, a Apostila contem erros e sua apresentacao didatico-peda-gogica deve ser revista. Neste sentido, agradecemos crıticas construtivas que serao objeto deanalise e reflexao e, por isto mesmo, muito bem-vindas.

Teresina-Pi, agosto de 2005.

Francisco Vieira de Souza

iv

Page 5: LF Apostila

Conteudo

Introducao viii

1 Programacao Funcional 11

1.1 Introducao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11

1.2 Historico . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12

1.3 Programacao com expressoes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13

1.4 Independencia da ordem de avaliacao . . . . . . . . . . . . . . . . . . . . . . . . . 14

1.5 Transparencia referencial . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16

1.6 Interfaces manifestas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18

1.7 Funcoes e expressoes aplicativas . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19

1.8 Definicao de funcoes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20

1.9 Resumo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25

2 λ-calculo 27

2.1 Introducao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27

2.2 λ-expressoes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28

2.3 A sintaxe do λ-calculo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29

2.3.1 Aplicacao de funcao e currificacao . . . . . . . . . . . . . . . . . . . . . . 29

2.4 Funcoes e constantes pre-definidas . . . . . . . . . . . . . . . . . . . . . . . . . . 30

2.5 λ-abstracoes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30

2.6 A semantica operacional do λ-calculo . . . . . . . . . . . . . . . . . . . . . . . . . 31

2.6.1 Formalizacao das ocorrencias livres ou ligadas . . . . . . . . . . . . . . . . 33

2.7 Combinadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33

2.8 Regras de conversoes entre λ-expressoes . . . . . . . . . . . . . . . . . . . . . . . 35

2.8.1 α-conversao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35

2.8.2 η-conversao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35

2.8.3 β-conversao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35

2.8.4 Nomeacao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36

2.8.5 Captura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36

2.9 Conversao, reducao e abstracao . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37

2.10 Provando a conversibilidade . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38

v

Page 6: LF Apostila

2.11 Uma nova notacao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38

2.12 Ordem de reducao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39

2.13 Funcoes recursivas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40

2.14 Resumo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42

3 Programacao funcional em Haskell 43

3.1 Introducao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43

3.2 Primeiros passos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44

3.2.1 O interpretador Hugs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45

3.2.2 Identificadores em Haskell . . . . . . . . . . . . . . . . . . . . . . . . . . . 47

3.3 Funcoes em Haskell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47

3.3.1 Construindo funcoes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49

3.3.2 Avaliacao de funcoes em Haskell . . . . . . . . . . . . . . . . . . . . . . . 51

3.3.3 Casamento de padroes (patterns matching) . . . . . . . . . . . . . . . . . 51

3.4 Tipos de dados em Haskell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52

3.4.1 Os tipos primitivos da linguagem . . . . . . . . . . . . . . . . . . . . . . . 52

3.4.2 Programando com numeros e strings . . . . . . . . . . . . . . . . . . . . . 59

3.4.3 Os tipos de dados estruturados de Haskell . . . . . . . . . . . . . . . . . . 60

3.4.4 Escopo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61

3.4.5 Calculos: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62

3.4.6 Projeto de programas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65

3.4.7 Provas de programas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65

3.5 Resumo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69

4 O tipo Lista 71

4.1 Funcoes sobre listas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72

4.1.1 O construtor de listas : (cons) . . . . . . . . . . . . . . . . . . . . . . . . 72

4.1.2 Construindo funcoes sobre listas . . . . . . . . . . . . . . . . . . . . . . . 73

4.2 Pattern matching revisado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75

4.3 Compreensoes e expressoes ZF (Zermelo-Fraenkel) . . . . . . . . . . . . . . . . . 78

4.4 Funcoes de alta ordem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84

4.4.1 A funcao map . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85

4.4.2 Funcoes anonimas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88

4.5 Polimorfismo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90

4.5.1 Tipos variaveis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91

4.5.2 O tipo mais geral . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91

4.6 Inducao estrutural . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92

4.7 Composicao de funcoes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95

4.7.1 Composicao avancada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96

vi

Page 7: LF Apostila

4.7.2 Esquema de provas usando composicao . . . . . . . . . . . . . . . . . . . . 96

4.8 Aplicacao parcial . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98

4.8.1 Secao de operadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99

4.8.2 Currificacao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99

4.9 Melhorando o desempenho de uma implementacao . . . . . . . . . . . . . . . . . 103

4.9.1 O desempenho da funcao reverse . . . . . . . . . . . . . . . . . . . . . . . 103

4.9.2 O desempenho do quicsort . . . . . . . . . . . . . . . . . . . . . . . . . . . 104

4.10 Resumo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106

5 Tipos de dados complexos 109

5.1 Introducao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109

5.2 Classes de tipos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109

5.2.1 Fundamentacao das classes . . . . . . . . . . . . . . . . . . . . . . . . . . 110

5.2.2 Funcoes que usam igualdade . . . . . . . . . . . . . . . . . . . . . . . . . . 111

5.2.3 Assinaturas e instancias . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112

5.2.4 Classes derivadas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112

5.2.5 As classes pre-definidas em Haskell . . . . . . . . . . . . . . . . . . . . . . 113

5.3 Tipos algebricos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115

5.3.1 Como se define um tipo algebrico? . . . . . . . . . . . . . . . . . . . . . . 116

5.3.2 A forma geral . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117

5.3.3 Derivando instancias de classes . . . . . . . . . . . . . . . . . . . . . . . . 118

5.3.4 Tipos recursivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118

5.3.5 Recursao mutua . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120

5.3.6 Tipos algebricos polimorficos . . . . . . . . . . . . . . . . . . . . . . . . . 121

5.4 Tratamento de erros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124

5.4.1 Valores fictıcios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124

5.4.2 Tipos de erros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125

5.5 Provas sobre tipos algebricos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127

5.6 Modulos em Haskell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128

5.6.1 Cabecalho em Haskell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129

5.6.2 Importacao de modulos . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129

5.6.3 O modulo main . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129

5.6.4 Controles de exportacao . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129

5.6.5 Controles de importacao . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130

5.7 Tipos abstratos de dados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130

5.7.1 O tipo abstrato Pilha . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131

5.7.2 O tipo abstrato de dado Fila . . . . . . . . . . . . . . . . . . . . . . . . . 133

5.7.3 O tipo abstrato Set . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136

5.7.4 O tipo abstrato Tabela . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137

vii

Page 8: LF Apostila

5.8 Lazy evaluation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139

5.8.1 Expressoes ZF (revisadas) . . . . . . . . . . . . . . . . . . . . . . . . . . . 140

5.8.2 Dados sob demanda . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141

5.8.3 Listas infinitas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141

5.9 Resumo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142

6 Programacao com acoes em Haskell 145

6.1 Introducao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145

6.2 Entrada e Saıda em Haskell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146

6.2.1 Operacoes de entrada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146

6.2.2 Operacoes de saıda . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147

6.2.3 A notacao do . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148

6.3 Arquivos, canais e descritores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149

6.3.1 A necessidade dos descritores . . . . . . . . . . . . . . . . . . . . . . . . . 150

6.3.2 Canais . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150

6.4 Gerenciamento de excecoes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150

6.5 Resumo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151

Referencias Bibliograficas 152

viii

Page 9: LF Apostila

Introducao

”Functional languages privide a frameworkin which the crucial ideas of modern programming

are presented in the clearest prossible way.”(Simon Thompson in [35])

Em 2004, Philip Wadler1 escreveu o artigo “Why no one uses functional languages”, onde elecomenta sobre a pouca utilizacao das linguagens funcionais na Industria e ambientes comerciais[37]. Para ele, dizer que ninguem usa linguagem funcional, e um exagero. As chamadas te-lefonicas no Parlamento Europeu sao roteadas por um programa escrito em Erlang, a linguagemfuncional da Ericsson. A rede Cornell distribui CDs virtuais usando o sistema Esemble, escritoem CAML e a Polygram vende CDs na Europa usando Natural Expert, da Software AG. Aslinguagens Erlang (www.erlang.se) e ML Works de Harlequin (www.harlequin.com) apresentamum extensivo ambiente de suporte ao usuario. Alem disso, as linguagens funcionais sao maisadequadas a construcao de provadores de teoremas, incluindo o sistema HOL que foi utilizadona depuracao do projeto de multiprocessadores da linha HP 9000.

Ainda segundo Wadler, as linguagens funcionais produzem um codigo de maquina com umamelhoria de uma ordem de magnitude e, nem sempre, estes resultados sao mostrados. Normal-mente, se mostram fatores de 4. Mesmo assim, um codigo que e quatro vezes menor, quatro vezesmais rapido de ser escrito ou quatro vezes mais facil de ser mantido nao pode ser jogado fora.Para ele, os principais fatores que influenciam na escolha de uma linguagem de programacaosao:

• Compatibilidade. Os sistemas nao sao mais construıdos monoliticamente, como eramno passado. Atualmente, eles sao escritos de forma modular. Os modulos sao construıdospor usuarios em tempos e locais possivelmente diferentes e sao ligados atraves de interfacesbem definidas. Muitas linguagens funcionais ja apresentam facilidades para a construcaode grandes softwares com formas bem adequadas de construcao de modulos e algumaslinguagens funcionais ainda nao oferecem estas facilidades. E necessario acabar com oisolamento das linguagens funcionais e incorporar a elas facilidades para a comunicacaoentre programas funcionais e programas codificados em outras linguagens pertencentesa outros paradigmas. A Industria da Computacao esta comecando a distribuir padroescomo CORBA e COM para suportar a construcao de software a partir de componentesreutilizaveis. Atualmente, os programas em Haskell ja podem ser empacotados como umcomponente COM e qualquer componente COM pode ser chamado a partir de Haskell.Entre outras aplicacoes, isto permitiu que Haskell fosse utilizada como linguagem para aconstrucao do Internet Explorer, o browser da Microsoft.

• Bibliotecas. Muitos usuarios escolheram Tcl, atraıdos, principalmente, pela bibliotecagrafica Tk. Muito pouco da atratividade de Java tem a ver com a linguagem em si,

1Philip Wadler trabalha nos grupos de ML e de Unix na Bell Labs. Ele e co-autor das linguagens Haskell eGJ. Alem de varios artigos publicados, ele tambem e co-editor da revista Journal of Functional Programming.

1

Page 10: LF Apostila

e sim com as bibliotecas associadas, usadas na construcao de graficos, banco de dados,interfaceamento, telefonia e servidores. Apesar de ainda nao existirem muitas bibliotecasgraficas para as linguagens funcionais, muito esforco tem sido feito nesta direcao, nosultimos tempos. Haskell tem Fudgets, Gadgets, Haggis e Hugs Tk. SML/NJ tem duas:eXene e SML Tk. Haskell e ML tem ambas um poderoso sistema de modulos que tornamsuas bibliotecas faceis de serem construıdas.

• Portabilidade. Inegavelmente C e C++ tem sido preferidas em muitos projetos. Noentanto, muito desta preferencia nao se deve ao fato de C gerar um codigo mais rapidoque o codigo gerado pelas linguagens funcionais, apesar de, normalmente, se verificar estadiferenca de desempenho. Na realidade, esta preferencia se deve mais a portabilidadeinegavel de C. Sabe-se que os pesquisadores em Lucent teriam preferido construir a lin-guagem PRL para Banco de Dados usando SML, mas escolheram C++, porque SML naoestava disponıvel no mainframe Amdahl, onde deveria ser utilizada. Por outro lado, astecnicas de implementacao de linguagens utilizando maquinas abstratas tem se tornadomuito atrativas para linguagens funcionais [21] e tambem para Java. Isto se deve muitoao fato de que escrever a maquina abstrata em C a torna muito mais facil de ser portadapara uma grande variedade de arquiteturas.

• Disponibilidade. Alguns compiladores sao muito difıceis de serem instalados. Por exem-plo, GHC (Glasgow Haskell Compiler) era considerado uma aventura por alguns usuariosque tentavam instala-lo. Ainda existem poucas linguagens funcionais comerciais e istotorna difıcil um codigo estavel e um suporte confiavel. Alem do mais, as linguagens funci-onais estao em permanente desenvolvimento e, portanto, estao sempre em transformacoes.

• Empacotamento. Muitas linguagens funcionais seguem a tradicao de LISP, de semprerealizar suas implementacoes atraves do loop read-eval-print. Apesar da conveniencia,e essencial desenvolver habilidades para prover alguma forma de conversao de programasfuncionais em programas de aplicacao standalone. Muitos sistemas ja oferecem isto, noentanto, incorporam o pacote de runtime completo a biblioteca e isto implica na exigenciade muita memoria.

• Ferramentas. Uma linguagem para ser utilizavel necessita de ferramentas para depuracaoe profiler. Estas ferramentas sao faceis de serem construıdas para linguagens estritas, noentanto, sao muito difıceis de serem construıdas para linguagens lazy, onde a ordem deavaliacao nao e conhecida a priori. Verifica-se uma excecao em Haskell, onde muitasferramentas de profiler ja estao disponıveis.

• Treinamento. Para programadores imperativos e muito difıcil programar funcionalmente.Uma solucao imperativa e mais facil de ser entendida e de ser encontrada em livros ouartigos. Uma solucao funcional demora mais tempo para ser criada, apesar de muito maiselegante. Por este motivo, muitas linguagens funcionais atuais proveem um escape para oestilo imperativo. Isto pode ser verificado em ML que nao e considerada uma linguagemfuncional pura, porque permite atribuicoes destrutivas. Haskell e uma linguagem funcionalpura, mas consegue imitar as atribuicoes das linguagens imperativas utilizando uma teoriafuncional complexa que e a semantica de acoes, implementadas atraves de monadas2.

• Popularidade. Se um gerente escolher uma linguagem funcional para ser utilizada emum projeto e este falhar, provavelmente ele sera crucificado. No entanto, se ele escolher Cou C++ e nao tiver sucesso, tem a seu favor o argumento de que o sucesso de C++ ja foiverificado em inumeros casos e em varios locais.

2A semantica de acoes e um tema tratado no Capıtulo 6. Os monadas nao sao aqui tratados, uma vez que seuestudo requer, como pre-requsito, o conhecimento aprofundado sobre implementacao de linguagens funcionais.

2

Page 11: LF Apostila

• Desempenho. Ha uma decada atras, os desempenhos dos programas funcionais erambem menores que os dos programas imperativos, mas isto tem mudado muito ultimamente.Hoje, os desempenhos de muitos programas funcionais sao melhores ou pelo menos estaoem “pes de igualdade” com seus correspondentes em C. Isto depende da aplicacao. Javatem uma boa aceitacao e, no entanto, seu desempenho e muito inferior a C, na grandemaioria das aplicacoes. Na realidade, existem linguagens com alto desempenho que naosao muito utilizadas e existem linguagens com desempenho mediano com alta taxa deutilizacao. Desempenho e um fator importante, mas nao tem se caracterizado como umfator decisivo na escolha de uma linguagem.

Resumidamente, existem muitos fatores que desencorajam a escolha de linguagens funcio-nais como forma de se codificar programas. Para ser fortemente utilizada, uma linguagem devesuportar trabalho interativo, possuir bibliotecas extensas, ser altamente portavel, ter uma im-plementacao estavel e facil de ser instalada, ter depuradores e profilers, ser acompanhada decursos de treinamentos e ja ter sido utilizada, com sucesso, em uma boa quantidade de projetos.

Para Wadler [37] todos estes requisitos ja sao perfeitamente atendidos por algumas linguagensfuncionais, por exemplo Haskell. Para ele o que ainda existe e um preconceito injustificavel porparte de alguns programadores de outros paradigmas de programacao. No entanto, para afelicidade e consolo dos admiradores das linguagens funcionais, esta cultura tem se modificado,e de forma rapida, ao longo dos ultimos anos.

A nosso ver, o que existe mesmo e a falta de informacao e conhecimento do que realmente eprogramacao funcional e quais as suas vantagens e desvantagens em relacao a programacao emoutros paradigmas. Esta Apostila tenta prover subsıdios para que seus usuarios possam iniciarum processo de discussao sobre o tema. Para desencadear este processo, e necessario comecarpelo entendimento da relacao entre a programacao funcional e a programacao estruturada.

Princıpios da programacao estruturada

Hoare enumerou seis princıpios fundamentais da estruturacao de programas [24]:

1. Transparencia de significado. Este princıpio afirma que o significado de uma expressao,como um todo, pode ser entendido em termos dos significados de suas sub-expressoes.Assim, o significado da expressao E + F depende simplesmente dos significados das sub-expressoes E e F, independente das complicacoes de cada uma delas.

2. Transparencia de propositos. Textualmente, Hoare diz: “o proposito de cada parteconsiste unicamente em sua contribuicao para o proposito do todo”. Assim, em E + F,o unico proposito de E e computar o numero que sera o operando esquerdo do operador“+”. Isto significa que seu proposito nao inclui qualquer efeito colateral.

3. Independencia das partes. Este princıpio apregoa que os significados de duas partes,nao sobrepostas, podem ser entendidos de forma completamente independente, ou seja, Epode ser entendida independentemente de F e vice versa. Isto acontece porque o resultadocomputado por um operador depende apenas dos valores de suas entradas.

4. Aplicacoes recursivas. Este princıpio se refere ao fato de que as expressoes aritmeticassao construıdas pela aplicacao recursiva de regras uniformes. Isto significa que se nossabemos que E e F sao expressoes, entao sabemos que E + F tambem e uma expressao.

5. Interfaces pequenas. As expressoes aritmeticas tem interfaces pequenas, porque cadaoperacao aritmetica tem apenas uma saıda e apenas uma ou duas entradas. Alem disso,

3

Page 12: LF Apostila

cada uma das entradas e saıdas e um valor simples. Assim, a interface entre as partes eclara, pequena e bem controlada.

6. Estruturas manifestas. Este princıpio se refere ao fato de que os relacionamentos es-truturais entre as partes de uma expressao aritmetica seja obvio. Uma expressao e umasub-expressao de uma expressao se estiver textualmente envolvida nela. Tambem duas ex-pressoes nao estao estruturalmente relacionadas se elas nao se sobrepuzerem de qualquerforma.

Estas caracterısticas sao verdadeiras em relacao a programacao estruturada, no entanto, nemsempre elas sao assim entendidas. A este respeito, John Hughes fez algumas reflexoes, anali-sando a importancia da programacao estruturada [15], fazendo um paralelo com as linguagensfuncionais. Em sua analise, ele cita que quando se pergunta a alguem o que e programacaoestruturada, normalmente se tem, uma ou mais, das seguintes respostas:

• e uma programacao sem gotos,

• e uma programacao onde os blocos nao tem entradas ou saıdas multiplas ou

• e uma programacao onde os programas nela escritos sao mais trataveis matematicamente.

Apesar de todas as respostas acima serem corretas, no que se refere a caracterizacao deste tipode programacao, elas nao sao conclusivas. Aludem ao que a programacao estruturada nao e,mas nao dizem o que realmente e a programacao estruturada. Na realidade, uma respostacoerente para a pergunta sobre a programacao estruturada pode ser: “programas estruturadossao programas construıdos de forma modular.”

Esta e uma resposta afirmativa e que atinge o cerne da questao. A construcao de programasmodulares e responsavel pela grande melhoria na construcao de software, sendo a tecnica res-ponsavel pelo notorio aumento de produtividade de software que ultimamente tem se verificado.Este aumento de produtividade se verifica porque:

• modulos pequenos podem ser codificados mais facilmente e mais rapidamente,

• modulos de proposito geral podem ser reutilizados, ou seja, maior produtividade e

• os modulos podem ser testados e compilados de forma independente, facilitando muito adepuracao dos programas.

Mas por que a modularidade e tao determinante? A resposta e imediata: na modularidade,os problemas sao decompostos em problemas menores e as solucoes para estes sub-problemas saomais faceis de serem encontradas. No entanto, estas pequenas solucoes devem ser combinadaspara representar uma solucao para o problema original como um todo.

Modula II, Ada, Pascal, C, C++, Standard ML, Haskell, Java, Eiffel e todas as modernas lin-guagens de programacao, independente do paradigma utilizado, foram projetadas ou adaptadasdepois para serem modulares.

O que e uma linguagem funcional?

Ainda segundo Hughes, um caso semelhante acontece quando se pergunta a alguem sobre oque e programacao funcional. Normalmente, se tem como resultado uma ou mais das seguintesrespostas:

4

Page 13: LF Apostila

• e uma linguagem onde os programas nela codificados consistem inteiramente de funcoes,

• e uma linguagem que nao tem side effects,

• e uma linguagem em que a ordem de execucao e irrelevante, ou seja, nao precisa analisaro fluxo de controle,

• e uma linguagem onde pode-se substituir, a qualquer tempo, variaveis por seus valores(transparencia referencial) ou

• e uma linguagem cujos programas nela escritos sao mais trataveis matematicamente.

Estas respostas tambem nao sao conclusivas, da mesma forma que as respostas dadas aquestao sobre programacao estruturada. E qual seria uma resposta afirmativa e definitiva?Usando a resposta anterior como base, pode-se dizer que ”as linguagens funcionais sao altamentemodularizaveis.”.

Esta e uma resposta afirmativa e que necessita apenas de mais um complemento para quea questao fique respondida em seu todo. Este complemento se refere as caracterısticas queas linguagens funcionais apresentam e que proporcionam esta melhoria na modularidade dossistemas. Estas caracterısticas podem ser sumarizadas na seguinte observacao: “a programacaofuncional melhora a modularidade, provendo modulos menores, mais simples e mais gerais,atraves das funcoes de alta ordem e lazy evaluation.

Estas duas caracterısticas, verificadas apenas nas linguagens funcionais, e que sao res-ponsaveis pela grande modularidade por elas proporcionada. Dessa forma, as linguagens funci-onais sao altamente estruturadas e se candidatam, com grande chance de exito, como solucoespara a tao propalada “crise do software” dos anos 80.

Para continuar esta viagem pelo mundo da programacao funcional, tentando entender suafundamentacao teorica e necessario conhecer tambem como as linguagens sao implementadas.Inicialmente vamos nos referir a implementacao de linguagens tradicionais e depois particulari-zaremos para o caso das funcionais.

O processo de compilacao de linguagens

Programar e modelar problemas do mundo real ou imaginario em um computador, usando algumparadigma de programacao. Desta forma, os problemas sao modelados em um nıvel de abstracaobem mais alto atraves de especificacoes formais, feitas utilizando uma linguagem de especificacaoformal. Existem varias linguagens de especificacao formal: Lotos, Z, VDM, Redes de Petri, entreoutras. A escolha de uma delas esta diretamente ligada ao tipo da aplicacao e a experienciado programador. Por exemplo, para especificar dispositivos de hardware e mais natural seusar Redes de Petri, onde pode-se verificar a necessidade de sincronizacao e podem ser feitassimulacoes para a analise de desempenho ou detectar a existencia, ou nao, de inconsistencias.

Muitas ferramentas graficas ja existem e sao utilizadas na analise de desempenho de dispo-sitivos especificados formalmente e podem ser feitos prototipos rapidos para se verificar a ade-quabilidade das especificacoes as exigencias do usuario, podendo estes prototipos serem parteintegrante do contrato de trabalho entre o programador e o contratante. Mais importante queisto, a especificacao formal representa um prova da corretude do programa e que ele faz exata-mente o que foi projetado para fazer. Isto pode ser comparado com a garantia que um usuariotem quando compra um eletrodomestico em uma loja. Esta garantia nao pode ser dada pelostestes, uma vez que eles so podem verificar a presenca de erros, nunca a ausencia deles. Ostestes representam uma ferramenta importante na ausencia de uma prova da corretude de um

5

Page 14: LF Apostila

programa, mas nao representam uma prova. O uso de testes requer que eles sejam bem proje-tados e de forma objetiva, para que tenham a sua existencia justificada. Com a especificacaopronta, ela deve ser implementada em uma linguagem de programacao.

Figura 1: Relacionamento entre Computador e Linguagens.

A linguagem de programacao a ser escolhida depende da aplicacao. Em muitos casos, esteprocesso e tao somente uma traducao, dependendo da experiencia do programador com a lingua-gem de programacao escolhida. No caso das linguagens funcionais, este processo e muito natural,uma vez que as especificacoes formais sao apenas definicoes implıcitas de funcoes, restanto ape-nas a traducao destas definicoes implıcitas para definicoes explıcitas na linguagem funcionalescolhida3. Resta agora a traducao destes programas em linguagens de alto nıvel para progra-mas executaveis em linguagem de maquina, sendo este o papel do compilador. Este processoesta mostrado na Figura 1.

A compilacao de programas codificados em linguagens imperativas, normalmente, e feita emduas etapas [1]. Na primeira delas, e feita uma traducao do programa escrito em linguagem dealto nıvel para um programa codificado em linguagem intermediaria, chamada de linguagem demontagem (Assembly). Na etapa seguinte, e feita a traducao do programa em linguagem demontagem para o programa executavel, em linguagem de maquina. Este processo esta mostradona Figura 2.

Figura 2: Processo de compilacao das linguagens imperativas.

3Estas formas de definicao de funcoes sao mostradas no Capıtulo 1 desta Apostila.

6

Page 15: LF Apostila

Esta mesma metodologia foi tentada por alguns pesquisadores na traducao de programas co-dificados em linguagens funcionais para programas em linguagem de maquina, mas os resultadosnao foram animadores [22]. Os codigos executaveis gerados eram todos de baixo desempenho.Por este motivo, os pesquisadores da area de implementacao de linguagens funcionais se viramobrigados buscar outras alternativas de compilacao para estas linguagens.

A implementacao de linguagens funcionais

Como ja mencionado anteriormente, as linguagens funcionais apresentam caracterısticas unicas,como as funcoes de alta ordem, polimorfismo e lazy evaluation. Estas caracterısticas sao propor-cionadas por particularidades apresentadas pela programacao aplicativa ou funcional. Estas par-ticularidades sao: a transparencia referencial, a propriedade de Church-Rosser, a independenciana ordem de avaliacao e as interfaces manifestas, que serao objeto de estudo no Capıtulo 1desta Apostila. Para que estas particularidades estejam presentes, e necessario que, durante aexecucao, muitas estruturas permanecam ativas na heap para que possam ser utilizadas maistarde. Estas caracterısticas tem impedido a criacao de codigos executaveis enxutos e de bonsdesempenhos.

Por este motivo, outras tecnicas de implementacao foram pesquisadas. Uma que tem apre-sentado resultados promissores consiste na traducao de programas codificados em linguagensfuncionais para programas em uma linguagem intermediaria, como na traducao das linguagensimperativas, mas utilizando λ-calculo4 como linguagem intermediaria, em vez da linguagem As-sembly [21]. Ja existem metodos eficientes de traducao de programas codificados no λ-calculopara programas em linguagem de maquina. Dessa forma, o problema agora se restringe atraducao dos programas escritos nas linguagens funcionais para programas em λ-calculo. Esteprocesso esta mostrado na Figura 3.

Figura 3: Um processo de compilacao das linguagens funcionais.

A escolha do λ-calculo como linguagem intermediaria entre as linguagens funcionais e ocodigo executavel se deve a dois fatores [29]:

1. o λ-calculo e uma linguagem simples, com poucos construtores sintaticos e semanticos e

2. o λ-calculo e uma linguagem suficientemente poderosa para expressar todos os programasfuncionais.

4λ-calculo e uma teoria de funcoes que sera vista no Capıtulo 2 desta Apostila.

7

Page 16: LF Apostila

Maquinas abstratas

Uma tecnica usada com sucesso na implementacao de linguagens funcionais consiste na imple-mentacao do λ-calculo usando a reducao das λ-expressoes para λ-expressoes mais simples, ateatingir uma forma normal, se ela existir. O resultado desta tecnica foi um sistema que ficouconhecido na literatura como maquinas abstratas. A primeira maquina abstrata foi desenvolvidapor Peter Landin em 1964 [20], que ganhou o alcunha de maquina SECD, devido ao seu nome(Stack, Environment, Code, Dump). A maquina SECD usa a pilha S para a avaliacao dasλ-expressoes Codificadas, utilizando o ambiente E.

Uma otimizacao importante na maquina SECD foi transferir alguma parcela do tempo deexecucao para o tempo de compilacao. No caso, isto foi feito transformando as expressoes comnotacao infixa para a notacao polonesa reversa que e adequada para ser executada em pilha,melhorando o ambiente de execucao. Para diferenciar da maquina SECD, esta maquina foichamada de SECD2.

Em 1979, David Turner [36] desenvolveu um processo de avaliacao de expressoes em SASL(uma linguagem funcional) usando o que ficou conhecida como a maquina de combinadores.Ele utilizou os combinadores S, K e I do λ-calculo5 para representar expressoes, em vez deλ-expressoes, utilizando para isto um dos combinadores acima citados, sem variaveis ligadas [7].Turner traduziu diretamente as expressoes em SASL para combinadores, sem passar pelo estagiointermediario das λ-expressoes. A maquina de reducao de Turner foi chamada de Maquinade Reducao SK e utilizava a reducao em grafos como metodo para sua implementacao. Estamaquina teve um impacto muito intenso na implementacao de linguagens aplicativas, pelo ganhoem eficiencia, uma vez que ela nao utilizava o ambiente da maquina SECD, mas tirava partidodo compartilhamento que conseguia nos grafos de reducao.

Um outro pesquisador que se tornou famoso no desenvolvimento de maquinas abstratas foiJohnsson, a partir de 1984, quando ele deu inıcio a uma serie de publicacoes [16, 17, 18] sobre estetema, culminando com sua Tese de Doutorado, em 1987 [19], onde ele descreve uma maquinaabstrata baseada em supercombinadores que eram combinadores abstraıdos do programa deusuario para algumas necessidades particulares. A maquina inventada por Johnsson ficou co-nhecida pela Maquina G e se caracterizou por promover uma melhoria na granularidade dosprogramas, que era muito fina na maquina de reducao de Turner. Uma otimizacao importantedesta maquina foi a utilizacao de uma segunda pilha na avaliacao de expressoes aritmeticas ououtras expressoes estritas.

Figura 4: O processo de compilacao das linguagens funcionais adotado em ΓCMC.

Varias outras maquinas abstratas foram construıdas com bons resultados. Entre elas podemser citadas a maquina GMC e a maquina Γ, idealizadas por Rafael Lins, da Universidade Federal

5Combinadores e supercombinadores sao temas do λ-calculo, objeto de estudo do Capıtulo 2, deste trabalho.

8

Page 17: LF Apostila

de Pernambuco [8].

Alem destas, uma que tem se destacado com excelentes resultados e a maquina ΓCMC,tambem de Rafael [21, 8], onde um programa codificado em SASL e traduzido para um programaem Ansi C. Este processo esta mostrado na Figura 4.

A escolha da linguagem C se deve ao fato de que os compiladores de C geram codigos reco-nhecidamente portateis e eficientes. A maquina ΓCMC e baseada nos combinadores categoricosque se fundamentam na Teoria das Categorias Cartesianas Fechadas, recentemente utilizada emdiversas areas da Computacao, sendo hoje um tema padrao do discurso, nos grandes encontrose eventos na area da Informatica [9].

Esta Apostila

Esta Apostila e composta desta Introducao e 6 (seis) Capıtulos. Nesta Introducao, e colocada,de forma resumida, a importancia das linguagens funcionais e a necessidade de estudar o λ-calculo e justificar sua escolha como a linguagem intermediaria entre as linguagens funcionaise as linguagens de maquina. E necessario saber que as linguagens funcionais sao importantesporque aumentam a modularidade dos sistemas atraves das funcoes de alto nıvel e do mecanismode avaliacao preguicosa.

O Capıtulo 1 e dedicado a fundamentacao das linguagens funcionais, abordando as princi-pais diferencas entre elas e as linguagens de outros paradigmas. O mundo das linguagens deprogramacao e dividido entre o mundo das expressoes e o mundo das atribuicoes, evidenciandoas vantagens do primeiro mundo em relacao ao segundo.

No Capıtulo 2, e introduzido o λ-calculo, sua evolucao historica e como ele e usado nos diasatuais. A teoria e colocada de maneira simples e introdutoria, dado o objetivo da Apostila.

No Capıtulo 3, inicia-se a programacao em Haskell. Sao mostrados seus construtores e umaserie de exemplos, analisando como as funcoes podem ser construıdas em Haskell. Sao mostradosos tipos de dados primitivos adotados em Haskell e os tipos estruturados mais simples que sao astuplas. No Capıtulo, tambem sao mostrados os esquemas de provas de programas, juntamentecom varios exercıcios, resolvidos ou propostos.

O Capıtulo 4 e dedicado a listas em Haskell. Este Capıtulo se torna necessario, dada aimportancia que este tipo de dado tem nas linguagens funcionais. Neste Capıtulo, sao mostradasas compreensoes ou expressoes ZF e tambem e mostrada a composicao de funcoes como umacaracterıstica apenas das linguagens funcionais, usada na construcao de funcoes. Um temaimportante e que e discutido neste Capıtulo se refere as formas de provas da corretude deprogramas em Haskell, usando inducao estrutural sobre listas. No Capıtulo sao mostradosvarios exemplos resolvidos e, ao final, sao colocados varios exercıcios a apreciacao do leitor.

No Capıtulo 5, sao mostradas as type class, como formas de incluir um determinado tipode dados em em uma classe de tipos que tenham funcoes em comum, dando origem a sobrecargacomo forma de polimorfismo. Tambem sao mostrados os tipos de dados algebricos, o sistema demodulos adotado em Haskell, os tipos de dados abstratos e o tratamento de excecoes. O Capıtulotermina com uma revisao sobre o sistema de avaliacao lazy, notadamente na construcao de listaspotencialmente infinitas.

O Capıtulo 6 e dedicado as operacoes de entrada e saıda em Haskell, evidenciado o uso dearquivos ou dispositivos como saıda ou como entrada. Este processo em Haskell e feito atravesdo mecanismo de “acoes”, cuja semantica representa o conteudo principal do Capıtulo.

A Apostila termina com as principais referencias bibliograficas consultadas durante s suaelaboracao.

9

Page 18: LF Apostila

10

Page 19: LF Apostila

Capıtulo 1

Programacao Funcional

”We can now see that in a lazy implementationbased on suspensions, we can treat every function in the same way.

Indeed, all functions are treated as potentially non-strictand their argument is automatically suspended.

Later, is and when it is needed, it will beunsuspended (strictly evaluated).”

(Antony D. T. Davie in [7])

1.1 Introducao

A programacao funcional teve inıcio antes da invencao dos computadores eletronicos. No inıciodo seculo XX, muitos matematicos estavam preocupados com a fundamentacao matematica, emparticular, queriam saber mais sobre os conjuntos infinitos. Muito desta preocupacao aconteceupor causa do surgimento, no final do seculo XIX, de uma teoria que afirmava a existencia de variasordens de infinitos, desenvolvida por George Cantor (1845-1918) [24]. Muitos matematicos, comoLeopold Kronecker (1823-1891), questionaram a existencia destes objetos e condenaram a teoriade Cantor como pura “enrolacao”. Estes matematicos defendiam que um objeto matematicoso poderia existir se, pelo menos em princıpio, pudesse ser construıdo. Por este motivo, elesficaram conhecidos como “construtivistas”.

Mas o que significa dizer que um numero, ou outro objeto matematico, seja construtıvel?Esta ideia foi desenvolvida lentamente, ao longo de muitos anos. Guiseppe Peano (1858-1932),um matematico, logico e linguista, escreveu “Formulaire de Mathematique” (1894-1908), ondemostrou como os numeros naturais poderiam ser construıdos atraves de finitas aplicacoes dafuncao sucessor. Comecando em 1923, Thoralf Skolen (1887-1963) mostrou que quase tudoda teoria dos numeros naturais poderia ser desenvolvido construtivamente pelo uso intensivode definicoes recursivas, como as de Peano. Para evitar apelos questionaveis sobre o infinito,pareceu razoavel chamar um objeto de construtıvel se ele pudesse ser construıdo em um numerofinito de passos, cada um deles requerendo apenas uma quantidade finita de esforco. Assim, nasprimeiras decadas do seculo XX, ja existia consideravel experiencia sobre as definicoes recursivasde funcoes sobre os numeros naturais.

A cardinalidade (quantidade de elementos) dos conjuntos finitos era facil de ser conhecida,uma vez que era necessario apenas contar seus elementos. Ja para os conjuntos infinitos, estatecnica nao podia ser aplicada. Inicialmente, era necessario definir o que era realmente umconjunto infinito. Foi definido que um conjunto era infinito se fosse possıvel construir umacorrespondencia biunıvoca entre ele e um subconjunto proprio dele mesmo. Foram definidos osconjuntos infinitos enumeraveis, que foram caracterizados pelos conjuntos infinitos para os quais

11

Page 20: LF Apostila

fosse possıvel construir uma correspondencia biunıvoca com o conjunto dos numeros naturais, N.Assim, todos os conjuntos infinitos enumeraveis tinham a mesma cardinalidade, que foi definidapor ℵ0

1. Assim, a cardinalidade do conjunto dos numeros inteiros, Z, tambem e ℵ0, uma vezque e possıvel construir uma correspondencia biunıvoca entre Z e N. Os conjuntos infinitos comos quais nao fosse possıvel estabelecer uma correspondencia biunıvoca entre eles e N, foramchamados de infinitos nao enumeraveis. A cardinalidade do conjuntos dos numeros reais, R,que e infinito nao enumeravel, e 2ℵ0 .

1.2 Historico

Na decada de 1930, existiram muitas tentativas de formalizacao do construtivismo, procurandocaracterizar o que era camputabilidade efetiva, ou seja, procurava-se saber o que realmente podiaser computado. Uma das mais famosas tentativas foi a definicao de Turing sobre uma classede maquinas abstratas, que ficaram conhecidas como “maquinas de Turing”, que realizavamoperacoes de leitura e escritas sobre uma fita de tamanho finito. Outra tecnica, baseada maisdiretamente nos trabalhos de Skolen e Peano, consistia no uso de “ funcoes recursivas gerais”,devida a Godel. Uma outra tecnica, com implicacao importante na programacao funcional, foia criacao do λ-calculo, desenvolvido por Church e Kleene, no inıcio da decada de 1930. Outranocao de computabilidade, conhecida como “Algoritmos de Markov”, tambem foi desenvolvidanesta mesma epoca. O que e importante e que todas estas nocoes de computabilidade foramprovadas serem equivalentes. Esta equivalencia levou Church, em 1936, a propor o que ficouconhecida como a Tese de Church2, onde ele afirmava que “uma funcao era computavel se elafosse primitiva recursiva” [5].

Isto significa que, ja na decada anterior a decada da invencao do computador eletronico,muitos matematicos e logicos ja haviam investigado, com profundidade, a computabilidade defuncoes e identificado a classe das funcoes computaveis como a classe das funcoes primitivasrecursivas.

O proximo invento importante na historia da programacao funcional foi a publicacao de JohnMcCarthy, em 1960, sobre LISP. Em 1958, ele investigava o uso de operacoes sobre listas ligadaspara implementar um programa de diferenciacao simbolica. Como a diferenciacao e um processorecursivo, McCarthy sentiu-se atraıdo a usar funcoes recursivas e, alem disso, ele tambem achouconveniente passar funcoes como argumentos para outras funcoes. McCarthy verificou que oλ-calculo provia uma notacao muito conveniente para estes propositos e, por isto mesmo, eleresolveu usar a notacao de Church em sua programacao.

Em 1958, foi iniciado um projeto no MIT com o objetivo de construir uma linguagem deprogramacao que incorporasse estas ideias. O resultado ficou conhecido como LISP 1, que foidescrita por McCarthy, em 1960, em seu artigo “Recursive Functions of Symbolic Expressionsand Their Computation by Machine” [24]. Este artigo mostrou como varios programas com-plexos podiam ser expressos por funcoes puras operando sobre estruturas de listas. Este fato ecaracterizado, por alguns pesquisadores, como o marco inicial da programacao funcional.

No final da decada de 1960 e inıcio da decada de 1970, um grande numero de cientistasda Computacao comecaram a investigar a programacao com funcoes puras, chamada de “pro-gramacao aplicativa”, uma vez que a operacao central consistia na aplicacao de uma funcao aseu argumento. Em particular, Peter Landin (1964, 1965 e 1966) desenvolveu muitas das ideiascentrais para o uso, notacao e implementacao das linguagens de programacao aplicativas, sendoimportante destacar sua tentativa de traduzir a definicao de uma linguagem nao funcional, Algol

1ℵ e uma letra do alfabeto arabe, conhecida por Aleph.2Na realidade, nao se trata de uma tese, uma vez que ela nunca foi provada. No entanto, nunca foi exibido

um contra-exemplo, mostrando que esta conjectura esteja errada.

12

Page 21: LF Apostila

60, para o λ-calculo. Baseados neste estudo, Strachey e Scott construiram um metodo de de-finicao da semantica de linguagens de programacao, conhecido como “semantica denotacional”.Em essencia, a semantica denotacional define o significado de um programa, em termos de umprograma funcional equivalente.

No entanto, a programacao aplicativa, que havia sido investigada por um reduzido numerode pesquisadores nos anos de 1960 e 1970, passou a receber uma atencao bem maior apos 1978,quando John Backus, o principal criador do FORTRAN, publicou um paper onde fez severascrıticas as linguagens de programacao convencionais, sugerindo a criacao de um novo paradigmade programacao. Ele propos o paradigma chamado de “programacao funcional” que, em essencia,e a programacao aplicativa com enfase no uso de funcionais, que sao funcoes que operam sobreoutras funcoes. Muitos dos funcionais de Backus foram inspirados pela linguagem APL, umalinguagem imperativa, projetada na decada de 1960, que provia operadores poderosos, sematribuicao, sobre estruturas de dados. A partir desta publicacao, o numero de pesquisadores naarea de linguagens de programacao funcional tem aumentado significativamente.

1.3 Programacao com expressoes

Para MacLennan [24] qualquer linguagem de programacao se divide em dois mundos, a saber:o mundo das expressoes (aritmeticas, relacionais, booleanas, etc.) e o mundo das atribuicoes.No primeiro caso, o unico objetivo e encontrar o valor de uma expressao atraves de um processode “avaliacao”. Ja no segundo caso, o mundo das atribuicoes e dividido em dois tipos. Oprimeiro altera o controle do fluxo de um programa, usando comandos de selecao, como if, for,while, repeat, goto e chamadas a procedimentos. O segundo tipo de atribuicao altera o estadoda memoria (principal ou secundaria) do computador. Nos dois tipos, a palavra chave e alteraralguma coisa; no primeiro tipo, altera o fluxo de controle e, no segundo, altera o estado damaquina.

No mundo das atribuicoes, a ordem em que as coisas sao feitas tem importancia fundamental.Por exemplo, e sequencia

i = i + 1;

a = a ∗ i;

tem um efeito diferente da sequencia, a seguir, onde a ordem e invertida.

a = a ∗ i;

i = i + 1;

Analisemos agora a expressao z = (2 ∗ a ∗ y + b) ∗ (2 ∗ a ∗ y + c). A expressao do lado direitodo sinal de atribuicao contem uma sub-expressao em comum (2 ∗ a ∗ y) e qualquer compilador,com um mınimo de otimizacao, transformaria este fragmento de codigo, na seguinte forma:

t = 2 ∗ a ∗ y;

z = (t + b) ∗ (t + c);

No mundo das expressoes, e seguro promover esta otimizacao porque qualquer sub-expressao,no caso 2 ∗ a ∗ y, sempre tem o mesmo valor. Analisemos agora, uma situacao similar no mundodas atribuicoes.

Sejam as expressoes

y = 2 ∗ a ∗ y + b; e z = 2 ∗ a ∗ y + c;

onde tambem verificamos uma sub-expressao em comum (2 ∗ a ∗ y). Se for realizada a mesmafatoracao anterior, teremos:

13

Page 22: LF Apostila

t = 2 ∗ a ∗ y;

y = t + b; z = t + c;

Esta otimizacao altera o valor da variavel z, porque os valores de y sao diferentes nas duasocorrencias da sub-expressao 2*a*y. Portanto, nao e possıvel realizar esta otimizacao. Apesarda analise sobre a existencia, ou nao, de dependencia entre sub-expressoes poder ser feita por umcompilador, isto requer tecnicas sofisticadas de analise de dependencias no fluxo. Tais analises,normalmente sao caras para serem realizadas e difıceis de serem implementadas corretamente.De forma resumida, e facil e seguro realizar estas otimizacoes nas expressoes e difıcil de seremfeitas no mundo das atribuicoes. Fica clara a vantagem das expressoes sobre as atribuicoes. Oproposito da programacao funcional e extender as vantagens das expressoes para as linguagensde programacao.

1.4 Independencia da ordem de avaliacao

Para entender as vantagens do mundo das expressoes e as fontes de suas propriedades, e ne-cessario investigar a ordem de avaliacao nas expressoes aritmeticas. Avaliar alguma coisa signi-fica encontrar seu valor. Assim, podemos avaliar a expressao aritmetica ‘5 ∗ 4 + 3’ encontrandoseu valor que, no caso, e 23.

Podemos tambem avaliar a expressao (3ax + b)(3ax + c)? A resposta e nao; a menos quesejam conhecidos os valores de a, b, c e x. O valor desta expressao e dependente de um contextoonde ela seja avaliada. Para isto, vamos avaliar esta expressao em um contexto em que a = 2,b = 3, c = −2 e x = 3. Esta avaliacao pode ser iniciada em varios pontos. Por motivo deregularidade, ela sera feita da esquerda para a direita, lembrando, no entanto, que ela tambempode ser realizada da direita para a esquerda. Colocando todos os operadores de forma explıcita,a expressao se transforma em:

(3 ∗ a ∗ x + b) ∗ (3 ∗ a ∗ x + c)

Para se realizar a primeira operacao, a multiplicacao 3 ∗ a, e necesario saber o valor de aque, neste contexto, e 2. Substituindo este valor na expressao, ela se torna

(3 ∗ 2 ∗ x + b) ∗ (3 ∗ a ∗ x + c)

Agora pode-se dar prosseguimento ao processo de avaliacao, substituindo a expressao poruma nova expressao:

(6 ∗ x + b ∗ (3 ∗ a ∗ x + c)

A sequencia completa de todos os passos realizados no processo de avaliacao e mostrada aseguir, onde a flexa dupla (⇒) significa a transformacao de expressoes.

(3 ∗ a ∗ x + b) ∗ (3 ∗ a ∗ x + c)

⇒ (3 ∗ 2 ∗ x + b) ∗ (3 ∗ a ∗ x + c)

⇒ (6 ∗ x + b) ∗ (3 ∗ a ∗ x + c)

⇒ (6 ∗ 3 + b) ∗ (3 ∗ a + x + c)

⇒ (18 + b) ∗ (3 ∗ a ∗ x + c)

⇒ (18 + 3) ∗ (3 ∗ a ∗ x + c)

⇒ 21 ∗ (3 ∗ a ∗ x + c)

⇒ 21 ∗ (3 ∗ 2 ∗ x + c)

⇒ 21 ∗ (6 ∗ x + c)

⇒ 21 ∗ (6 ∗ 3 + c)

14

Page 23: LF Apostila

Figura 1.1: Arvore representativa da expressao (3ax+b)(3ax+c).

Figura 1.2: Representacao do processo de avaliacao.

⇒ 21 ∗ (18 + c)

⇒ 21 ∗ (18 + (−2))

⇒ 21 ∗ 16

⇒ 336

Observe que se a avaliacao tivesse sido iniciada pelo operando da direita do operador demultiplicacao, ‘∗’, o resultado seria exatamente o mesmo, ou seja, qualquer ordem de avaliacaoproduziria o mesmo resultado, 336. Isto ocorre porque, na avaliacao de uma expressao pura3,a avaliacao de uma sub-expressao nao afeta o valor de qualquer outra sub-expressao, uma vezque nao existe qualquer dependencia entre elas. Na realidade, e possıvel realizar as avaliacoesdestas sub-expressoes de forma concorrente. E facil entender esta independencia da ordem deavaliacao, utilizando uma arvore de avaliacao, conforme pode ser visualizado na Figura 1.1, ondeas variaveis ficam nas folhas e as operacoes nos nos internos.

Nesta estrutura, cada operacao em um no depende apenas das operacoes dos nos abaixodele na arvore. A avaliacao de uma sub-arvore afeta apenas a parte da arvore acima destasub-arvore, ou seja, nao afeta as sub-arvores que estejam a sua esquerda ou a sua direita. Oprocesso de avaliacao e iniciado com a colocacao de alguns valores nas folhas. Os nos internos saoavaliados em qualquer ordem, sob demanda, podendo ate mesmo serem avaliados em paralelo.Cada operacao depende apenas de suas entradas que sao os valores dos nos filhos. Este processode avaliacao pode ser visto graficamente na Figura 1.2.

A avaliacao de um no consiste na “decoracao” de cada no interno com o valor resultanteda avaliacao dos nos abaixo dele. O processo de avaliacao termina quando a raiz da arvore fordecorada com o valor da expressao completa. Isto pode ser visto na arvore da Figura 1.3.

3Uma expressao e dita pura quando nao realiza qualquer operacao de atribuicao, explıcita ou implıcita.

15

Page 24: LF Apostila

Figura 1.3: Estado da arvore apos a avaliacao total da expressao.

Como afirmado anteriormente, diversos processos podem acontecer em paralelo, na decoracaoda arvore, desde que seja observada a estrutura de arvore. Isto significa que sempre se chega aomesmo valor.

Esta propriedade verificada nas expressoes puras, ou seja, a independencia da ordem de ava-liacao, e chamada de “propriedade de Church-Rosser”. Ela permite a construcao de compiladorescapazes de escolher a ordem de avaliacao que faca o melhor uso dos recursos da maquina. Apossibilidade de que a avaliacao seja realizada em paralelo, implica na possibilidade de utilizacaode multiprocessadores de forma bastante natural.

Por outro lado, as “expressoes impuras”, normalmente, nao apresentam esta propriedade,conforme pode ser verificado no exemplo a seguir, em Pascal. Seja a expressao a+2*F(b). Elae pura ou impura? Para responder a isso, devemos verificar a definicao de F. Por exemplo,

function F(x : Integer) : Integer;begin

F := x * xend;

Como F nao executa qualquer atribuicao, a nao ser a pseudo-atribuicao a F para retornar ovalor da funcao, ela e uma funcao pura. Isto significa que, na avaliacao da expressao a+F(b),pode-se avaliar a sub-expressao a ou 2*F(b), que o resultado sera o mesmo. No entanto, vamossupor que F fosse definida da seguinte forma:

function F(x : Integer) : Integer;begin

a := a + 1;F := x * x

end;

Neste caso, F e chamada de pseudo-funcao, porque ela nao e uma funcao pura. Supondoque a variavel a seja a mesma variavel da expressao a+2*F(b), como F altera o valor de a, ovalor de a+2*F(b) depende de qual operando do operador ‘+’ e avaliado em primeiro lugar.Se, por exemplo, o valor de a for zero, caso ele seja avaliado primeiro, o valor da expressao sera2b2, ao passo que se 2*F(b) for avaliada primeiro, a expressao tera o valor 2b2 + 1.

1.5 Transparencia referencial

Vamos novamente considerar o contexto de avaliacao da secao anterior. Podemos verificar quese uma pessoa fosse avaliar manualmente a expressao (3ax + b)(3ax + c) jamais iria avaliar asub-expressao 3ax, que neste contexto e 18, duas vezes. Uma vez avaliada esta sub-expressao, o

16

Page 25: LF Apostila

Figura 1.4: Grafo com um no compartilhado.

avaliador humano substituiria a sub-expressao 3ax por 18, em todos os casos onde ela aparecesse.A avaliacao seria feita da seguinte forma:

(18 + b) ∗ (18 + c)

⇒ (18 + 3) ∗ (18 + c)

⇒ 21 ∗ (18 + (−2))

⇒ 21 ∗ 16

⇒ 336

Isto acontece porque a avaliacao de uma mesma expressao, em um contexto fixo sempre daracomo resultado o mesmo valor. Para os valores de a = 2 e x = 3, 3ax sera sempre igual a 18.

Este processo pode ser entendido observando a arvore de avaliacao da expressaomostradana Figura 1.4. Como a sub-expressao 3ax ocorre duas vezes, nao existe razao para se duplicara representacao na arvore. Neste caso, pode-se simplesmente rotear os dois arcos que usam asub-expressao para a mesma sub-arvore.

A rigor, nao se tem mais uma estrutura de arvore, e sim um grafo acıclico. No entanto,pode-se decorar o grafo partindo das folhas, da mesma maneira feita antes. A unica diferencae que apos a decoracao do no compartilhado, seu valor pode ser usado por ambos os nos acimadele.

Esta propriedade e chamada de “transparencia referencial”, e significa que, em um contextofixo, a substituicao de sub-expressoes por seus valores e completamente independente da ex-pressao envolvente. Portanto, uma vez que uma expressao tenha sido avaliada em um dadocontexto, nao e mais necessario avalia-la novamente porque seu valor jamais sera alterado. Deforma mais geral, a transparencia referencial pode ser definida como “a habilidade universal desubstituir iguais por iguais”. Em um contexto em que a = 2 e x = 3, sempre pode-se substituir3ax por 18 ou 18 por 3ax, sem que o valor da expressao envolvente seja alterado. A transparenciareferencial resulta do fato de que os operadores aritmeticos nao tem memoria e, assim sendo,toda chamada a um operador com as mesmas entradas produz sempre o mesmo resultado.

Mas por que a transparencia referencial e importante? Da Matematica, sabemos da im-portancia de poder substituir iguais por iguais. Isto conduz a derivacao de novas equacoes, apartir de equacoes dadas e a transformacao de expressoes em formas mais usuais e adequadaspara a prova de propriedades sobre elas.

No contexto das linguagens de programacao, a transparencia referencial permite otimizacoescomo a eliminacao de sub-expressoes comuns. Por exemplo, dada a segunda definicao da pseudo-funcao F, da secao anterior, e claro que, como F deixa em a o registro do numero de vezes queela e chamada, nao se poderia eliminar a sub-expressao comum, F(b), da expressao

(a+2*F(b))*(c+2*F(b))

17

Page 26: LF Apostila

Isto acontece porque a troca do numero de vezes que F e chamada altera o resultado daexpressao. Em algumas linguagens de programacao, isto complica a eliminacao de sub-expressoescomuns.

1.6 Interfaces manifestas

A evolucao da notacao matematica ao longo dos anos tem permitido que ela seja utilizadacom sucesso, para exibir muitas propriedades. Uma destas propriedades se refere as interfacesmanifestas, ou seja, as conexoes de entradas e saıdas entre uma sub-expressao e a expressao quea envolve. Consideremos a expressao “3 + 8”. O resultado desta adicao depende apenas dasentradas para a operacao (3 e 8) e elas estao mostradas de forma clara na expressao, ou seja,sao colocadas a esquerda e a direita do operador “+”. Nao existem entradas escondidas paraeste operador.

Vamos agora considerar o caso de uma funcao em uma linguagem de programacao conven-cional, onde o resultado a ser retornado pela funcao depende de uma ou mais variaveis locaisou nao locais. Assim, chamadas sucessivas com as mesmas entradas podem produzir resultadosdistintos. Por exemplo, seja a funcao f definida da seguinte forma:

function f (x : Integer) : Integer;begin

a := a + 1;f := a * x

end;

Nao existe uma forma de se conhecer o valor de f(3) sem antes saber o valor da variavel naolocal, a. O valor de a e atualizado em cada chamada a f, entao f(3) tem valores diferentes emcada chamada. Esta situacao caracteriza a existencia de interfaces escondidas para a funcao f,tornando difıcil, ou mesmo impossıvel, se prever o comportamento da funcao.

Consideremos, novamente, a expressao (2ax + b) ∗ (2ax + c). Como pode ser observado, asentradas para o primeiro operador “+” (2ax e b) estao manifestas. O papel da sub-expressao2ax+b, dentro da expressao completa, tambem e manifesto, ou seja, ela representa o argumentoesquerdo do operador de multiplicacao. Nao existem saıdas escondidas ou side effects na adicao.Portanto, tanto as entradas como as saıdas desse operador sao determinadas muito facilmente.As entradas sao as sub-expressoes 2ax e b em qualquer lado do operador e a saıda e liberadapara a expressao envolvente.

Pode-se sumarizar as caracterısticas das interfaces manifestas, da seguinte forma: as ex-pressoes podem ser representadas por arvores e a mesma arvore representa tanto a estruturasintatica de uma expressao quanto a forma como os dados fluem na expressao. As sub-expressoesque se comunicam entre si podem sempre ser colocadas em posicoes contıguas na arvore, comona forma escrita da expressao. Esta caracterıstica nao e valida no mundo das atribuicoes, umavez que as variaveis alteraveis permitem comunicacao nao local. Em geral, o grafico do fluxode dados nao e uma arvore e pode ser uma estrutura muito diferente da arvore sintatica. As-sim, pode nao ser possıvel colocar juntas as partes comunicantes, de forma que suas interfacessejam obvias. A identidade estrutural das dependencias de dados e das dependencias sintaticase certamente uma das principais vantagens das expressoes puras.

18

Page 27: LF Apostila

1.7 Funcoes e expressoes aplicativas

Vamos agora analisar as avaliacoes das expressoes aritmeticas. Estas expressoes sao estru-turalmente simples, ou seja, sao construıdas de forma uniforme pela aplicacao de operacoesaritmeticas a seus argumentos. Mais importante que isto, estas operacoes sao funcoes puras, ouseja, sao mapeamentos matematicos de entradas para saıdas. Isto significa que o resultado deuma operacao depende apenas de suas entradas. Alem disso, uma expressao construıda a partirde funcoes puras e constantes tem sempre o mesmo valor. Por exemplo, seja a funcao pura fdefinida da seguinte forma:

f(u) = (u + b)(u + c)

em um contexto em que b = 3 e c = 2. A funcao f e pura porque e definida em termos de funcoespuras que sao as operacoes aritmeticas de adicao e multiplicacao. Vamos considerar agora, aavaliacao de f(3ax) em um contexto em que a = 2 e x = 3. Como a avaliacao de expressoesaritmeticas independe da ordem de avaliacao, assim tambem sera a avaliacao de f . Isto significaque o argumento (3ax) de f pode ser avaliado antes ou depois da substituicao; o resultado serasempre o mesmo. Se ele for avaliado antes, a sequencia de reducoes sera:

f(3ax) ⇒ f(3 ∗ 2 ∗ 3) ⇒ f(18) ⇒ (18 + b) ∗ (18 + c) ⇒ (18 + 3) ∗ (18 − 2) ⇒ 21 ∗ 16 ⇒ 336

Se, no entanto, a avaliacao de 3ax for deixada para ser feita apos a substituicao, a sequenciade reducoes sera

f(3ax) ⇒ (3ax + b) ∗ (3ax + c) ⇒ (3 ∗ 2 ∗ 3 + b) ∗ (3ax + c) ⇒ . . . ⇒ 336

Estas duas sequencias de avaliacao correspondem aos metodos de passagem de parametropor valor e por nome, respectivamente, nas linguagens de programacao comuns. A passagemde parametros por valor, normalmente, e mais eficiente porque o argumento e avaliado apenasuma vez, no entanto o que e mais importante e o fato de que o valor da expressao e o mesmo,independente da ordem de avaliacao adotada. No entanto, apesar da ordem de avaliacao es-colhida nao ter influencia no valor final da expressao, ela pode influenciar sobre o termino, ounao, do processo de avaliacao. Por exemplo, a avaliacao da funcao f(x) ≡ 1 para x = 1/a emum contexto em que a seja igual a zero tem diferenca nas duas ordens de avaliacao. Se 1/a foravaliado antes da substituicao a avaliacao sera indefinida e nao termina. Se for deixada para serfeita depois da substituicao, o resultado sera 1. No primeiro caso, a avaliacao e dita “estrita” eno segundo caso, ela e dita “nao estrita” em relacao a seu argumento x.

A programacao aplicativa, ou funcional, e frequentemente distinguida da programacao im-perativa que adota um estilo que faz uso de imperativos ou ordens. O mundo das atribuicoes ecaracterizado tambem por ordens, por exemplo, “troque isto!”, “va para tal lugar!”, “substituaisto!” e assim por diante. Ao contrario, o mundo das expressoes envolve a descricao de valores,por isto, o termo “programacao orientada por valores”.

Na programacao aplicativa, os programas tomam a forma de expressoes aplicativas. Umaexpressao deste tipo e uma constante (2, π, e, etc) ou e composta totalmente de aplicacao defuncoes puras a seus argumentos, que tambem sao expressoes aplicativas. Em BNF, as expressoesaplicativas sao definidas da seguinte forma:

<EA> ::= <id> (<EA>, ...)| <literal>| <id>

A estrutura aplicativa se torna mais clara se as expressoes forem escritas na forma pre-fixa,ou seja, sum(prod (prod (2, a), x), b) em vez da forma usual 2ax+b.

19

Page 28: LF Apostila

A programacao aplicativa tem um unico construtor sintatico que e a aplicacao de umafuncao a seu argumento. Na realidade, este construtor e tao importante que, normalmente,e representado de forma implıcita, por justaposicao, em vez de explicitamente, atraves de algumsımbolo. Desta forma, sen x significa a aplicacao da funcao sen ao argumento x.

As vantagens da programacao sem atribuicoes em relacao a programacao com atribuicoes saosimilares as da programacao sem gotos em relacao a programacao com gotos. Resumidamente,sao elas:

• os programas sao mais faceis de serem entendidos,

• os programas podem ser derivados mais sistematicamente e

• e mais facil de serem feitas inferencias sobre eles.

As vantagens da programacao funcional sobre a programacao imperativa podem ser resumi-das nos seguintes argumentos:

1. A programacao funcional conduz a uma disciplina que melhora o estilo.

2. A programacao funcional encoraja o programador a pensar em nıveis mais altos de abs-tracao, atraves de mecanismos como funcoes de alta ordem, lazy evaluation e polimorfismo.

3. A programacao funcional representa um paradigma de programacao para a computacaomassivamente paralela, pela ausencia de atribuicoes, pela independencia da ordem deavaliacao e pela habilidade de operar estruturas completas de dados.

4. A programacao funcional e uma aplicacao da Inteligencia Artificial.

5. A programacao funcional e importante na criacao de especificacoes executaveis e na im-plementacao de prototipos com uma rigorosa fundamentacao matematica. Isto permiteverificar se as especificacoes estao corretas, ou nao.

6. A programacao funcional esta fortemente acoplada a teoria da computacao.

1.8 Definicao de funcoes

A programacao funcional usando a linguagem Haskell e o principal objetivo desta Apostila. Istosignifica que devemos estudar formas de passagens das funcoes matematicas para funcoes co-dificadas em Haskell. Do ponto de vista matematico, as funcoes sao definidas sobre conjuntos,domınio e imagem, mas nao se tem qualquer preocupacao sobre as formas como elas sao execu-tadas para encontrar um valor resultado. Ja do ponto de vista da computacao, esta preocupacaoexiste e e muito importante. No mundo da computacao, as funcoes sao declaradas sobre tipose leva-se em conta o algoritmo utilizado para implementa-las. A diferenca de desempenho deum algoritmo em relacao a um outro, definido para executar a mesma funcao, tem importanciafundamental no mundo da computacao. Apesar desta diferenca de pondo de vista entre estesdois mundos, o processo de passagem de um para o outro e quase um processo de traducaodireta. Assim, neste Capıtulo, as funcoes serao definidas como na Matematica e deixamos aanalise do ponto de vista computacional para ser feita no Capıtulo 3.

Definicoes de funcoes por enumeracao

Uma funcao e uma associacao de valores pertencentes a um conjunto de partida, o domınio, comvalores pertencentes a um conjunto de chegada, o contra-domınio ou imagem da funcao. Com

20

Page 29: LF Apostila

esta definicao em mente, uma funcao pode ser representada de duas maneiras. A primeira delas eexibir todos os pares do tipo “(entrada, saıda)”, sendo esta uma definicao extensionista, tambemconhecida como definicao por enumeracao, uma vez que todos os seus elementos sao exibidos. Asegunda maneira de representar uma funcao e exibir uma propriedade que apenas os elementosdesta funcao a tem. Isto significa a exibicao de uma regra que informa como cada elemento dodomınio e processado, transformando-o em um valor que e associado a um unico elemento daimagem. Esta forma de apresentacao de uma funcao e conhecida como intencionista.

Por exemplo, a funcao booleana not tem como domınio e contra-domınio o conjunto {True,False} e pode ser definida, por extensao ou enumeracao, da seguinte forma:

not True = Falsenot False = True

De forma similar, as funcoes or (disjuncao) e and (conjuncao) tambem podem ser definidaspor enumeracao, da seguinte maneira:

or (False, False) = False and (False, False) = Falseor (False, True) = True e and (False, True) = Falseor (True, False) = True and (True, False) = Falseor (True, True) = True and (True, True) = True

As definicoes de funcoes por intencionalidade serao vistas a seguir, em mais detalhes, uma vezque esta e a forma mais usual de apresentacao, dadas as varias formas como elas se apresentam.

Definicao de funcoes por composicao

Nao e difıcil entender que as definicoes de funcoes por enumeracao so tem sentido se o domınioe o contra-domınio forem finitos e de cardinalidade pequena. A grande maioria das funcoes naopode ser definida desta forma. Uma alternativa e definir as funcoes pela composicao de outrasfuncoes ja definidas. Como exemplo, a funcao implica pode ser definida da seguinte forma:

implica (x, y) = or (not x, y)

Neste caso, e necessario saber como as funcoes a serem compostas sao aplicadas. A aplicacaode uma funcao se torna simplesmente um processo de substituicao das funcoes primitivas. Porexemplo, para avaliar a funcao implica (False, True) e necessario apenas fazer a substituicaodos argumentos pelas aplicacoes das funcoes primitivas.

implica (False, True) = or (not False, True) = or (True, True) = True

Este processo e independente do domınio, ou seja, e independente de quando se esta tratandocom funcoes sobre numeros, ou funcoes sobre caracteres, ou funcoes sobre arvores, ou qualqueroutro tipo. Em outras palavras, se a funcao f for definida por

f(x) = h(x, g(x))

entao sabemos que

f(u(a)) = h(u(a), g(u(a)))

independente das definicoes de g, h, u ou da constante a.

Frequentemente, a definicao de uma funcao nao pode ser expressa pela composicao simplesde outras funcoes, sendo necessaria a definicao da funcao para varios casos. Por exemplo, afuncao que retorna o sinal algebrico de uma variavel pode ser definida da seguinte forma:

21

Page 30: LF Apostila

sinalg(x) =

1, se x > 00, se x = 0−1, se x < 0

De forma similar, a diferenca absoluta entre x e y pode ser definida da seguinte forma:

difabs(x, y) =

{x − y, se x > yy − x, se x ≤ y

Definicao de funcoes por recursao

Algumas situacoes existem em que e necessario definir uma funcao em termos de um numeroinfinito de composicoes. Por exemplo, a multiplicacao de numeros naturais (×) pode ser definidapor infinitas aplicacoes da funcao de adicao (+). Senao vejamos:

m × n =

0, se m = 0n, se m = 1n + n, se m = 2n + n + n, se m = 3...

Como nao e possıvel escrever um numero infinito de casos, este metodo de definicao defuncoes so e usual se existir alguma “regularidade” ou algum “princıpio de unificacao” entreos casos, que permita gerar os casos nao definidos, a partir dos casos ja definidos. Se existirtal princıpio de unificacao, ele deve ser estabelecido, sendo este o proposito das “definicoesrecursivas”, onde um objeto e definido em termos de si proprio. Por exemplo, uma definicaorecursiva da multiplicacao anterior pode ser:

m × n =

{0, se m = 0n + (m − 1) × n, se m > 0

Neste caso, uma avaliacao de 2 × 3 e feita por substituicao da seguinte forma:

2×3 ⇒ 3+(2−1)×3 ⇒ 3+1×3 ⇒ 3+3+(1−1)×3 ⇒ 3+3+0×3 ⇒ 3+3+0 ⇒ 3+3 ⇒ 6

A recursao e o metodo basico de se fazer alguma operacao iterativamente.

Exercıcios.

1. Avalie a expressao 3 × 5, usando a definicao recursiva da multiplicacao mostrada nestasecao.

2. Defina, recursivamente, em termos da multiplicacao definida nesta secao, a funcao potencia,onde uma base e elevada a um expoente inteiro nao negativo.

3. Defina, recursivamente, a exponenciacao de numeros nao negativos elevados a uma potenciainteira. Sugestao: use a definicao condicional e a resposta do exercıcio anterior.

4. Defina, recursivamente, a adicao de inteiros nao negativos, em termos das funcoes sucessore predecessor.

5. Defina, recursivamente, a adicao de inteiros quaisquer.

6. Defina, recursivamente, a divisao por inteiros nao negativos, em termos das funcoes sub-tracao e menor que.

22

Page 31: LF Apostila

Definicao explıcita de variaveis

Vamos considerar agora as definicoes explıcitas de funcoes, analisando inicialmente, as definicoesexplıcitas de variaveis. Uma definicao de uma variavel e explıcita se ela aparece no lado esquerdoda equacao e nao aparece no lado direito. Por exemplo, a equacao

y = 2ax

define explicitamente a variavel y. As definicoes explıcitas tem a vantagem de que elas podemser interpretadas como regras de reescritas que nos informam como substituir uma classe deexpressoes por outra. Por exemplo, a definicao anterior de y, implica na regra de reescritay ⇒ 2ax que nos diz como eliminar a variavel y em qualquer formula, onde y ocorra. Porexemplo, para eliminar y da expressao 3y2 + 5y + 1 aplica-se a regra de reescrita acima, para seobter

3y2 + 5y + 1 ⇒ 3(2ax)2 + 5(2ax) + 1

A nocao de definicao explıcita de variaveis pode ser extendida aos conjuntos de equacoessimultaneas. Um conjunto de variaveis e definido explicitamente por um conjunto de equacoes,se

• as operacoes forem individualmente explıcitas e

• elas puderem ser ordenadas, de forma que nenhuma delas use em seu lado direito umavariavel ja definida anteriormente na lista.

Por exemplo, o conjunto de equacoes

y = 2 × a × xx = 2a = 3

define explicitamente y, x e a. As equacoes precedentes podem ser convertidas as seguintesregras de reescrita:

y ⇒ 2 × a × xx ⇒ 2a ⇒ 3

Estas regras podem ser aplicadas na seguinte ordem: a primeira delas pode ser aplicada ateque nao exista mais y, em seguida a segunda e aplicada ate que nao exista mais x e, finalmente,a terceira e aplicada ate que nao exista mais a. Desta forma teremos a seguinte sequencia dereducoes:

y ⇒ 2 × a × x ⇒ 2 × a × 2 ⇒ 2 × 3 × 2

Definicao implıcita de variaveis

Dizemos que uma variavel e definida implicitamente se ela for definida por uma equacao ondeela aparece nos dois lados da equacao. Por exemplo,

2a = a + 3

define implicitamente a como 3. Para encontrar o valor de a e necessario resolver a equacaousando regras da algebra. O processo de solucao pode ser visto como uma forma de converteruma definicao implıcita em uma definicao explıcita, mais usual, uma vez que uma definicaoimplıcita nao pode ser convertida diretamente em uma regra de reescrita. Assim, a equacao

23

Page 32: LF Apostila

2a = a + 3

nao nos informa explicitamente o que deve ser substituıdo por a na expressao 2ax, por exemplo.Alem disso, as regras de reescrita que resultam de definicoes explıcitas sempre terminam, ou seja,a aplicacao repetida das regras de reescritas elimina todas as ocorrencias da variavel definida.

Por outro lado, e possıvel escrever definicoes implıcitas que nao terminam, ou seja, nadadefinem. Considere, como exemplo, a definicao implıcita

a = a + 1

Apesar de sabermos, claramente, que esta equacao nao tem solucao, este fato pode nao sertao obvio, em casos mais complexos. Se, ingenuamente, interpretarmos esta equacao como aregra de reescrita

a ⇒ a + 1

entao chegaremos a um nao determinismo na seguinte sequencia de reducoes:

2a ⇒ 2(a + 1) ⇒ 2((a + 1) + 1) ⇒ . . .

As variaveis tambem podem ser definidas implicitamente por conjuntos de equacoes si-multaneas. Por exemplo, as equacoes

2a = a + 3

d − 1 = 3d + a

definem implicitamente a = 3 e d = −2. Podemos tambem ter definicoes implıcitas em que asvariaveis nao aparecem nos dois lados da mesma equacao. Por exemplo, as equacoes

2a = x

x + 1 = a + 4

em que, nem a nem x aparece nos dois lados de uma mesma equacao, definem implicitamentea e x. Neste caso, nao existe qualquer forma de ordenacao destas equacoes de maneira queas ultimas equacoes nao facam uso de variaveis ja definidas nas equacoes anteriores. A formaimplıcita pode ser observada pela transformacao das duas equacoes em uma so, ou seja,

2a + 1 = a + 4

Em resumo, uma definicao explıcita nos informa o que uma determinada “coisa” e, enquantouma definicao implıcita estabelece algumas propriedades que esta “coisa” deve apresentar, exi-gindo que apenas esta “coisa” tenha estas propriedades. A determinacao do que esta coisa eexige um processo de solucao.

Definicoes explıcitas e implıcitas de funcoes

Apos termos analisado as definicoes explıcitas e implıcitas das variaveis, vamos agora considerarcomo estas definicoes sao aplicadas ao caso das funcoes. Por exemplo, as duas equacoes, a seguir,definem implicitamente a funcao implica.

and [p, implica (p, q)] = and (p, q)and [not p, implica (p, q)] = or [not p, and (not p, q)]

Estas equacoes nao podem ser usadas explicitamente para avaliar uma expressao como im-plica (True, False). Usando a algebra booleana, estas equacoes podem ser resolvidas para sechegar a seguinte definicao explıcita:

implica (p, q) = or (not p, q)

24

Page 33: LF Apostila

A definicao explıcita permite que implica (True, False) seja avaliada usando substituicao.Uma vantagem da programacao funcional e que, como a algebra elementar, ela simplifica atransformacao de uma definicao implıcita em uma definicao explıcita. Isto tem uma importanciamuito forte porque as especificacoes formais de sistemas de softwares, frequentemente, tem aforma de definicoes implıcitas, enquanto as definicoes explıcitas sao, normalmente, faceis deserem transformadas em programas. Assim, a programacao funcional prove uma forma de sepassar das especificacoes formais para programas satisfazendo estas especificacoes.

Exercıcios.

1. Mostre que a definicao explıcita da funcao implica (anterior) satisfaz a sua definicaoimplıcita.

2. Mostre que a definicao explıcita da funcao implica e a unica solucao para a sua definicaoimplıcita, ou seja, nenhuma outra funcao booleana satisfaz estas duas equacoes, apesar deque devem existir outras formas de expressar esta mesma funcao. Sugestao: usar Tabela-verdade.

Deve ser notado que as definicoes recursivas sao implıcitas, por natureza. No entanto, comoseu lado esquerdo e simples, ou seja, composto apenas pelo nome da funcao e seus argumentos,elas podem ser convertidas facilmente em regras de reescritas. Por exemplo, as duas equacoesseguintes constituem uma definicao recursiva de fatorial, para n ≥ 0.

fat n = n × fat (n-1), se n > 0fat 0 = 1

elas podem ser convertidas as seguintes regras de reescritas:

fat n ⇒ n × fat (n-1), se n > 0fat 0 ⇒ 1

Estas regras de reescritas nos dizem como transformar uma formula contendo fat. A rea-lizacao destas transformacoes, no entanto, nao elimina, necessariamente, a funcao da formula.Por exemplo,

2 + fat 3 ⇒ 2 + 3 × fat (3 − 1)

No entanto, se a computacao termina, entao a aplicacao repetida das regras de reescritaeliminara fat da formula

2 + 3 × fat 2 ⇒ 2 + 3 × 2 × fat 1⇒ 2 + 3 × 2 × 1 × fat 0⇒ 2 + 3 × 2 × 1 × 1

1.9 Resumo

Este Capıtulo se relaciona com a fundamentacao teorica das linguagens funcionais. Esta funda-mentacao e fortemente conectada com a Matematica e isto tem como vantagem primeiramente aconstatacao de essas linguagens tem um embasamenteo teorico sob o qual elas se fundamentame depois, sendo este embasamento oriundo da Matematica, torna estas linguagens mais trataveismatematicamente e mais faceis de terem seus programas provados.

O objetivo deste Capıtulo foi mostrar a programacao funcional como uma programacao sematribuicoes, da mesma maneira que a programacao estruturada e uma programacao sem gotos.Uma maquina, ao executar um programa estruturado, esta, de fato, executando gotos, enquanto

25

Page 34: LF Apostila

uma maquina, ao executar um programa funcional, esta, de fato, realizando atribuicoes. Emambos os casos, estas construcoes sao escondidas do programador, ou seja, elas estao em umnıvel inferior de abstracao.

A parte historica mostrada no inıcio do Capıtulo foi baseada na Apostila de Rafael Lins[22] e no livro de Brainerd [5]. A fundamentacao teorica foi, grande parte, baseada no livro deBruce Maclennan [24] que e um livro composto de duas partes, a primeira sendo pratica e asegunda teorica. Ele adota esta metodologia de praticar e depois justificar a parte pratica. Estametodologia e inusitada e faz com que o livro seja uma referencia muito interessante nao apenaspelo metodo utilizado mas, principalmente, pelo conteudo muito bem escrito.

Outro livro fundamental neste estudo foi o livro de Antony Davie [7] que apresenta umconteudo proximo da parte teorica do livro de Maclennan. O Capıtulo tambem se baseia nosensinamentos do livro de Chris Okasaki [26] que apresenta uma teoria de linguagens funcionaisbastante profunda e objetiva.

26

Page 35: LF Apostila

Capıtulo 2

λ-calculo

”Type theories in general date backto the philosopher Bertrand Russell and beyond.

They were used in the early 1900’s for the very specificpurpose of getting round the paradoxes that had shaken

the foundations of mathematics at that time,but their use was later widened until they came to be part of

the logicians´ standard bag of techinica tools, especially in proof-theory.”(J. Roger Hindley in [13])

2.1 Introducao

O λ-calculo foi desenvolvido por Alonzo Church no inıcio dos anos 30, como parte de um sistemade logica de ordem superior com o proposito de prover uma fundamentacao para a matematica,dentro da filosofia da escola logicista de Peano-Russel [22]. O λ-calculo e uma teoria que res-surge do conceito de funcao como uma regra que associa um argumento a um valor calculadoatraves de uma transformacao imposta pela definicao da funcao. Nesta concepcao, uma funcaoe representada por um grafo, onde cada no e um par (argumento, valor).

A logica combinatorial foi inventada antes do λ-calculo, em cerca de 1920, por MosesSchonfinkel, com um proposito semelhante ao do λ-calculo [5]. No entanto, ela foi redesco-berta 10 anos depois, em 1930, por Haskell B. Curry, que foi o maior responsavel pelo seudesenvolvimento ate cerca de 1970. Em 1935, Kleene e Rosser provaram que o λ-calculo e alogica combinatorial eram inconsistentes, o que provocou um desinteresse academico pelo tema.No entanto, Curry nao desistiu de seu estudo e construiu uma extensao na logica combinatorialpara fins de seus estudos, conseguindo sistemas mais fracos que a logica classica de segundaordem. Em 1975, Dana Scott e Peter Aczel mostraram que seu ultimo sistema apresentavamodelos interessantes [5]. Em 1941, Church desistiu de sua pesquisa inicial e apresentou umasub-teoria que lidava somente com a parte funcional. Essa sub-teoria, chamada de λ-calculofoi mostrada ser consistente pelo teorema de Church-Rosser [22]. Usando o λ-calculo, Churchpropos uma formalizacao da nocao de computabilidade efetiva pelo conceito de definibilidadeno λ-calculo. Kleene mostrou que ser definıvel no λ-calculo e equivalente a recursividade deGodel-Herbrand. Concomitantemente, Church formulou a sua conjectura associando a recursi-vidade como a formalizacao adequada do conceito de computabilidade efetiva. Em 1936, AllanTuring modelou a computacao automatica e mostrou que a nocao resultante (computabilidadede Turing) e equivalente a definibilidade no λ-calculo. Desta forma, o λ-calculo pode ser vistocomo uma linguagem de programacao. Isto implica que podem-se analisar diversos tipos deproblemas de programacao, em particular, os relacionados com chamadas a procedimentos [22].

27

Page 36: LF Apostila

Curry e seus colegas [6], Barendregt [3] e outros desenvolveram extensivamente os aspectossintaticos do λ-calculo, enquanto Scott [32], principalmente reportado por Stoy [34] e Schmidt[31], se dedicou a semantica da notacao, o que veio a facilitar a vida dos usuarios futuros doλ-calculo.

2.2 λ-expressoes

O λ-calculo tem apenas quatro construcoes: variaveis, constantes, aplicacoes e abstracoes. Su-ponhamos que sejam dadas uma sequencia infinita de sımbolos distintos chamados de variaveis euma sequencia finita, infinita ou vazia de sımbolos distintos, chamados constantes [29]. Quandoa sequencia de constantes for vazia, o sistema e chamado de puro; em caso contrario, e chamadode aplicado. O conjunto de expressoes, chamadas de λ-expressoes, e definido indutivamente daseguinte forma:

• todas as variaveis e constantes sao λ-expressoes (chamadas de atomos);

• se M e N forem λ-expressoes, entao (MN) tambem e uma λ-expressao, chamada com-binacao ou aplicacao;

• se M for uma λ-expressao e x for uma variavel qualquer, entao (λx.M) tambem e umaλ-expressao, chamada abstracao ou funcao.

As λ-expressoes, assim definidas, podem ser formuladas utilizando a notacao BNF. Isto efeito da seguinte maneira, onde elas sao representadas por < exp >:

< exp > :: < constante > - constantes embutidas|< variavel > - nomes de variaveis| (< exp >< exp >) - combinacao ou aplicacao| (λ < variavel > . < exp >) - abstracao ou funcao.

Apesar das definicoes acima serem claras, elas sao muito rıgidas e, am alguns casos, podemsurgir duvidas sobre as regras de escopo, ou seja, ate que ponto em uma λ-expressao uma variaveltem influencia. Assim, as observacoes a seguir devem ser utilizadas para dirimir duvidas, poracaso existentes.

Observacoes importantes:

1. As variaveis sao representadas por letras romanas minusculas.

2. As λ-expressoes completas sao representadas por letras romanas maiusculas ou por letrasgregas minusculas (exceto α, β, γ e λ que tem significados especiais) ou por letras gregasmaiusculas.

3. Apesar da rigidez mostrada na BNF das λ-expressoes, os parenteses devem ser usa-dos apenas para resolver ambiguidades [38]. Esta e uma convencao que diminui muitoo tamanho das λ-expressoes e deve ser utilizada, sempre que possıvel. Por exemplo,(λx.(λy.(. . . (λz.(E)) . . .))) pode e deve ser escrita como λxy . . . z.E, significando que gru-pos de abstracoes ou funcoes sao associados pela direita. No entanto, grupos de com-binacoes ou aplicacoes termos combinados sao associados pela esquerda, ou seja, E1E2E3 . . . En

significa (. . . ((E1E2)E3) . . . En). Alem disso, λa.CD representa (λa.CD) e nao (λa.C)(D),estabelecendo que o escopo de uma variavel se extende ate o primeiro parentese descasadoencontrado a partir da variavel, ou atingir o final da λ-expressao.

4. A escolha de constantes, de funcoes embutidas para serem utilizadas na manipulacao destasconstantes e/ou numeros e das funcoes para o processamento de listas e arbitraria.

28

Page 37: LF Apostila

Estas observacoes sao importantes porque elas podem ser utilizadas para dirimir algumasduvidas que, com certeza, vao existir, principalmente por quem esta travando um primeirocontacto com o processo de avaliacao no λ-calculo. E comum nao se reconhecer imediatamenteo escopo de uma variavel e estas observacoes podem ser um meio importante para dirimir estasduvidas.

Exemplos

Sao exemplos de λ-expressoes:

1. y

2. 2

3. xy

4. (λx . (xy))

5. ((λy . y) (λx . (xy)))

6. (x (λx . y))x

7. (λx . λy . λz . (xz)) (λx . x)(λx . x)

2.3 A sintaxe do λ-calculo

Nesta secao sera mostrada a sintaxe do λ-calculo, ficando a semantica para uma proxima.

• Todas as aplicacoes de funcoes sao pre-fixas, significando que, por exemplo, a expressao(+(∗ 2 3) (∗ 8 2)) tem seus parenteses externos redundantes podendo, e ate devendo,serem retirados para evitar confusao visual.

• Do ponto de vista de implementacao, um programa funcional deve ser visto como umaexpressao que vai ser avaliada.

• Esta avaliacao se da pela selecao repetida de expressoes redutıveis, conhecidas como rede-xes, e suas reducoes. Por exemplo, a λ-expressao (+(∗ 2 3) (∗ 8 2)) apresenta dois redexes:um (∗ 2 3) e o outro (∗ 8 2), apesar da expressao toda nao representar um redex porqueseus parametros ainda nao estao todos avaliados. A avaliacao da λ-expressao acima e feitacom a seguinte sequencia de reducoes:(+(∗ 2 3) (∗ 8 2)) → (+ 6(∗ 8 2)) → (+ 6 16) → 22

2.3.1 Aplicacao de funcao e currificacao

A aplicacao de uma funcao f a um parametro x e denotada apenas por justaposicao, ou seja,f x . E se a funcao tiver mais de um argumento? Por exemplo, a λ-expressao (+ 3 4) einterpretada como (+ 3) 4, ou seja, como uma funcao (+ 3) que adiciona 3 ao seu argumento(4). Este resultado se deve ao fato de que o λ-calculo permite que o resultado da aplicacao deuma funcao seja tambem uma outra funcao. Este resultado foi descoberto por Schonfinkel, masfoi amplamente utilizado por Curry e, por este motivo, passou a ser conhecido como currificacao.

Dito de outra forma, currificacao e uma caracterıstica muito importante de algumas lingua-gens, onde uma funcao com n argumentos pode ser interpretada como n funcoes de apenas 1(um) argumento. Isto significa que, para o λ-calculo, todas as funcoes tem apenas um argumento.

29

Page 38: LF Apostila

2.4 Funcoes e constantes pre-definidas

Uma caracterıstica importante do λ-calculo e a facilidade com que as funcoes podem ser cons-truıdas pelo usuario. No entanto, algumas delas juntamente com algumas constantes ja forampre-definidas e que podem ser utilizadas pelos usuarios. Entre elas podem ser citadas:

• funcoes matematicas: (+, -, *, /),

• constantes (0, 1, 2, ...), NIL

• funcoes logicas (AND, OR, NOT)

• constantes (TRUE, FALSE)

• caracteres constantes (‘a’, ‘b’, ...)

• funcao condicional IF: IF TRUE E1 E2 → E1

IF FALSE E1 E2 → E2

• construtores de dados: (CONS, HEAD, TAIL) ondeHEAD (CONS a b) → aTAIL (CONS a b) → b

Exemplos:

a) − 5 4 → 1

b) AND TRUE FALSE → FALSE

2.5 λ-abstracoes

As funcoes embutidas no λ-calculo sao formalizadas como ja mostrado anteriormente. No en-tanto, deve existir uma forma de construir funcoes que nao sejam embutidas. Isto e feito atravesde um construtor (λ). Uma λ-abstracao e um tipo particular de expressao que denota umafuncao. Por exemplo,(λx . + x 1) e uma λ-abstracao que deve ser interpretada da seguinteforma:

λ indica que se trata de uma funcaox sobre a variavel x. que+ x 1 adiciona x ao numero 1.

Uma λ-abstracao tem sempre estes 4 (quatro) elementos: o construtor (λ), o parametroformal (uma variavel, no caso, x) o ponto (.) e o corpo da funcao (+ x 1). Uma λ-abstracao podeser comparada com as funcoes em uma linguagem de programacao imperativa. Por exemplo, aλ-abstracao anterior pode ser representada em C pelo seguinte fragmento de codigo:

int inc (x)int x;{ return (x + 1) };

Deve ser observado que as funcoes em uma linguagem de programacao convencional tem deter um nome, enquanto, no λ-calculo, as λ-abstracoes sao funcoes anonimas.

30

Page 39: LF Apostila

2.6 A semantica operacional do λ-calculo

A semantica operacional do λ-calculo diz respeito as regras utilizadas para converter uma λ-expressao em outra. Na realidade, existem 3 (tres) regras de conversao. Antes de serem mos-tradas estas regras, devemos definir alguns termos que sao utilizados na aplicacao destas regras.

Uma ideia central no estudo das linguagens de programacao, da notacao matematica e dalogica simbolica e a de ocorrencia livre e ocorrencia ligada (ou conectada) de uma variavel. Estaideia, normalmente, provoca confusao em quem esta vendo o tema pela primeira vez. Assim, elesera introduzido de forma gradual e intuitiva, utilizando exemplos ja conhecidos da Matematica.Assim, vamos considerar o somatorio:

n∑i=1

i2 + 1

Nesta expressao, a variavel i e uma variavel ligada, ou seja, ela esta fortemente atreladaao somatorio. Diz-se que ela ocorre ligada nesta expresssao. Uma das caracterısticas inerentesas variaveis ligadas e que elas podem ser renomeadas sem que o significado da expressao sejaalterado. Por exemplo, o mesmo somatorio anterior pode tambem ser representado da seguinteforma, sem haver qualquer modificacao em seu significado:

n∑k=1

k2 + 1

De forma identica, a integral

∫ 1

0x2 − 3xdx

com respeito a x, representa a mesma integral

∫ 1

0t2 − 3tdt

agora relacionada a variavel t. Na teoria dos conjuntos, o conjunto de todos x tal que x ≥ 0 e omesmo conjunto de todos os y tal que y ≥ 0, ou seja,

{x | x ≥ 0} ≡ {y | y ≥ 0}

Tambem a proposicao “para todo x, x+1>x” e equivalente a proposicao “para todo y,y+1>y”, ou seja,

∀x[x + 1 > x] ≡ ∀y[y + 1 > y]

Vejamos agora, uma outra expressao, tambem envolvendo somatorio:

in∑

j=1

(j2 + i − a)

Nesta expressao, a ocorrencia da variavel j e ligada. Isto pode ser verificado pelo fato deque ela pode ser trocada por qualquer outra variavel, desde que nao seja i ou a, sem mudar emnada o significado da expressao. Por exemplo, a expressao

31

Page 40: LF Apostila

in∑

k=1

(k2 + i − a)

tem, exatamente, o mesmo significado da anterior. Em uma expressao, uma ocorrencia naoligada de uma variavel e dita ser “livre”. As variaveis i, a e n ocorrem livre nesta expressao.Se uma ocorrencia de uma variavel livre for trocada, o significado da expressao tambem seratrocado. Alem disso, uma ocorrencia de uma variavel pode ser ligada em uma expressao e livreem outra. Por exemplo, a variavel i e livre em

n∑j=1

j2 + i − a

e emn∑

j=1

ai2j2

mas e ligada na expressao

m∑i=1

i!

n∑j=1

j2 + i − a

O local da vinculacao (binding site) de um identificador determina seu escopo, que e a regiaoda expressao na qual o identificador ocorre ligado. Esta regiao, normamente, e indicada poralguma convencao lexica, como os parenteses, colchetes ou chaves. Ainda nesta secao, sera vistoque esta regiao e o corpo da expressao.

Como visto, a troca de variaveis ligadas nao interfere no significado da expressao. No entanto,esta troca nao pode ser feita para uma variavel que ocorra livre na expressao, caso contrario aexpressao muda seu significado. Por exemplo, a expressao que representa a soma dos elementosda linha i de uma matriz Am×n e o somatorio

n∑j=1

Aij

A variavel j ocorre ligada nesta expressao e, neste caso, pode ser trocada por outra, porexemplo, k.

n∑k=1

Aik

cujo significado e o mesmo da expressao anterior. No entanto, observemos que a variavel j naopode ser trocada pela variavel i. Neste caso, a expressao se tornaria o somatorio de Aii, querepresenta o somatorio dos elementos da diagonal da matriz e nao mais dos elementos da linhai, como inicialmente. Este fenomeno e conhecido como “colisao de identificadores” e deve serevitado.

Fazendo um paralelo com as linguagens de programacao comuns, as variaveis ligadas dasexpressoes correspondem exatamente aos parametros formais das funcoes. Na Matematica, afuncao f(x) ≡ x2 − 3x tem x como variavel ligada e como seu parametro formal.

32

Page 41: LF Apostila

2.6.1 Formalizacao das ocorrencias livres ou ligadas

Apos termos visto as nocoes de ocorrencias ligada e livre, como tambem a nocao de colisao deidentificadores de maneira totalmente informal, vamos agora formalizar estes conceitos.

Seja a λ-expressao (λx . + x y) 4. Esta expressao informa que se trata de uma funcao sobrea variavel x, onde o corpo desta funcao e (+ x y).

A variavel x pode ser pensada como um local onde o argumento 4 deve ser colocado. Japara a variavel y, este mesmo raciocınio nao pode ser aplicado, uma vez que nao existe estelocal sinalisado pela variavel y seguida apos um λ. Isto implica que as duas variaveis (x e y)tem status distintos.

Formalmente, define-se: “uma ocorrencia de uma variavel e ligada se existir uma λ-abstracaoa qual esta variavel esteja ligada. Em caso contrario, a ocorrencia e livre.” A Figura 2.1 mostraum exemplo detalhado de variaveis livres e ligadas.

λ x . +((λ y . + y z) 7) x

ocorrencias: ligada livre ligada

Figura 2.1: Exemplos de ocorrencias livres e de ocorrencias ligadas de variaveis.

Deve ser observado que uma mesma variavel pode ter uma ocorrencia livre e outra(s) li-gada(s). Por exemplo, na λ-expressao + x ((λx . + x 1) 4), a primeira ocorrencia de x e livre ea segunda e ligada ou conectada.

Podemos dizer, sendo x e y duas variaveis e θ e δ duas λ-expressoes, que:

a) x e livre em y se x = y. Se x = y, a ocorrencia de x e ligada.b) x e livre em λy . θ se (x for livre em θ) E (x = y)c) x e livre em θ δ se (x for livre em θ) OU (x for livre em δ)

Exemplos

1. x ocorre livre em x, em xy, em λa . xy e em (λa . xy) (λx . xy).

2. x ocorre ligada em y, em λx . xy, em (λx . ax) (y), em λx . abbx e em (λa . xy)(λx . xy).

3. Nao existem variaveis livres nos combinadores I, K, S, ∆, Y, Θ (ver secao 2.7).

2.7 Combinadores

Uma λ-expressao que nao apresenta variaveis livres e chamada fechada ou combinador. Existemalguns combinadores que desempenham um papel especial no estudo do λ-calculo, conforme foiafirmado na Introducao desta Apostila, onde foi feita referencia aos combinadores S, K e I, nosquais e baseada a maquina de reducao-SK de Turner. Podemos tambem citar:

I = λx.x IdentidadeK = λx.λy.x ProjecaoS = λx.λy.λz.xz(yz) Composicao∆ = λx.xx DuplicacaoY = λf.(λy.f(yy))(λy.f(yy)) Usado na representacao de funcoes recursivasΘ = λa.λb.b(aab) Tambem usado na representacao de funcoes recursivas

33

Page 42: LF Apostila

Tabela 2.1: ResumoUma ocorrencia de uma variavel deve ser livre ou ligada.Definicao de ocorrencia li-vre:

x ocorre livre em xx ocorre livre em (E F ) ⇔ x ocorre livre em E ou

x ocorre livre em Fx ocorre livre em λy . E ⇔ x e y sao variaveis distintas e

x ocorre livre em E

Definicao de ocorrencia li-gada:

x ocorre ligada em (E F ) ⇔ x ocorre ligada em E oux ocorre ligada em F

x ocorre ligada em λy . E ⇔ (x e y sao a mesma variavel ex ocorre livre em E) oux ocorre ligada em E.

Exercıcios resolvidos

1. Identifique nas expressoes abaixo aquelas que sao, ou nao, λ-expressoes:a) a Sim, a e uma variavel;b) 9 Sim, 9 e uma constante;c) ((λb.b)(λa.ab) Nao, os parenteses nao estao aninhados

corretamente;d) (λx.)a Nao, λx. nao e um termo;e) ((λx.λy.y)(λy.yyy))((λi.i)(λa.b)) Sim, porque contem apenas aplicacoes,

abstracoes e variaveis.

2. Identifique nas expressoes abaixo as ocorrencias livres e as ligadas das variaveisa) λx.xx As duas ocorrencias da variavel x sao conectadasb) (λx.λy.x)x O ultimo x ocorre livrec) (λx.λy.xx)xa As duas ultimas variaveis ocorrem livresd) (x(λx.y))x Todas as variaveis ocorrem livres

Exercıcios propostos

1. Justifique porque as expressoes abaixo nao sao λ-expressoes:

(a) (x(λy)z)

(b) (λx.λy.x((λi.ii)(λb.λc.b)(λb.λc.b))

(c) λ

(d) λx.λx

2. Identifique as ocorrencias livres e as ocorrencias ligadas das variaveis nas expressoes abaixo:

(a) (λx.xwx)9

(b) (λx.λy.x)3b

(c) (λx.yxx)(λi.i)5

(d) (λz.λb.λc.ac(bc)f)(λb.λc.b)(λb.λc.b)

(e) (λx.λy.y)w((λz.zzz)(λw.www))((λa.a)(λa.b))

3. Quais das seguintes expressoes sao combinadores?

34

Page 43: LF Apostila

(a) (λx.xx)9

(b) (λx.λy.x)3

(c) (λx.yxx)(λi.i)5

(d) (λa.λb.λc.ac(bc)f)(λb.λc.b)

(e) (λx.λy.y((λz.zzz)(λw.ww))(λa.a)(λa.x))

2.8 Regras de conversoes entre λ-expressoes

A forma de avaliacao utilizada no λ-calculo consiste em um processo de transformacao de umaλ-expressao em outra λ-expresssao mais simples. Este processo e continuado ate atingir umaλ-expressao que nao pode mais ser transformada em outra mais simples, ou entrar em loopinfinito. Esta secao e devotada a analise das tecnicas que sao utilizadas para promover estastransformacoes. Na realidade elas tem um papel similar ao que as tecnicas algebricas tem nasolucao de equacoes da Matematica.

Existem tres tecnicas basicas utilizadas na conversao de uma λ-expressao em outra. Saoelas: a α-conversao, a η-conversao e a β-conversao.

2.8.1 α-conversao

Ao analisarmos as duas λ-expressoes (λx .+ x 1) e (λy . + y 1), verificamos que elas apresentamo mesmo comportamento, ou seja, a unica diferenca entre elas esta nos nomes dos parametros,significando que elas representam a mesma λ-expressao. Deduz-se, portanto, que estas duasλ-expressoes sao convertıveis uma na outra. Esta conversao e chamada de α-conversao que nospermite trocar os nomes dos parametros formais (variaveis ligadas) de qualquer λ-abstracao.Assim,

(λx . ∗ x 1) α←→(λy. ∗ y 1)

2.8.2 η-conversao

Analisemos, agora, estas duas λ-abstracoes: (λx . + 1 x) e (+ 1). Verificamos que elas secomportam exatamente da mesma maneira quando aplicadas a um argumento. Assim, elastambem sao convertıveis uma na outra, ou seja,

(λx . + 1 x) η←→(+ 1) ou, mais formalmente,(λx . F x)

η←→F , desde que x nao ocorra livre em F e F seja uma funcao.

Exemplos:

1. a λ-expressao (λx . + xx) nao e η-redutıvel a (+ x) porque a variavel x ocorre livre em(+ x).

2. (λx . TRUE x) nao e η-redutıvel a TRUE porque TRUE nao e uma funcao.

2.8.3 β-conversao

O resultado da aplicacao de uma λ-abstracao a um argumento e uma instancia do corpo daλ-abstracao na qual as ocorrencias livres do parametro formal no corpo da λ-abstracao saotrocadas por copias do argumento.

35

Page 44: LF Apostila

Exemplo: a aplicacao da λ-abstracao (λx . + x 1) 4 reduz-se para + 4 1 que e uma instanciado corpo + x 1, onde trocamos a ocorrencia livre de x pelo argumento 4. Esta operacao echamada de β-reducao.

Exemplos:

1. (λx . + xx) 5β−→+ 5 5 = 10

2. (λx . 3) 5 β−→3

3. (λx . (λy . − y x))4 5β−→(λy . − y 4)5

β−→ − 5 4 = 1.

Observacoes:

1. Nas reducoes acima, deve ser observada a currificacao em acao, ou seja, as aplicacoes dasλ-abstracoes retornando uma outra funcao (λ-abstracao) como resultado.

2. Normalmente se abrevia (λx . (λy . E)) por λx . λy . E.

3. Para o caso de se ter uma funcao como argumento, as substituicoes sao realizadas damesma forma. Por exemplo,

(λf . f 3)(λx . + x 1)β−→(λx . + x 1)3

β−→+ 3 1 = 4

2.8.4 Nomeacao

Deve ser tomado algum cuidado na escolha dos nomes dos parametros, uma vez que mais deuma variavel pode ter o mesmo nome. Por exemplo,

(λx . (λx . + (− x 1))x 3)9 se β-reduz para (λx . + (− x 1))9 3 que se β-reduz para +(− 9 1)3que e igual a + 8 3 que, finalmente, e igual a 11.

Deve ser observado que o x interno de (− x 1) da primeira linha deste exemplo nao foisubstituıdo, uma vez que ele nao ocorre livre em (λx . + (− x 1)) que e o corpo da λ-abstracaomais externa.

2.8.5 Captura

O uso de nomes de variaveis pode algumas vezes criar situacoes confusas envolvendo β-reducoes.Por exemplo, seja a λ-expressao (λx . (λy . yx))y. O y mais externo desta λ-expressao ocorrelivre. Se executarmos uma β-reducao vamos obter a λ-expressao λy . yy. Esta λ-expressao resul-tante apresenta um significado distinto da anterior porque o y externo, que era livre, tornou-seconectado. Esta situacao e conhecida como o problema de captura de variaveis. A possibilidadede captura causa complicacoes para a avaliacao mecanica de λ-expressoes. A solucao trivial paraeste problema e efetuarem-se α-conversoes antes de se realizar a β-conversao para se evitar quevariaveis distintas, mas homonimas, sejam confundidas. No caso da λ-expressao citada, umasequencia correta de reducoes seria:

(λx . (λy . yx))y α−→(λx . (λz . zx))y β−→λz . zy

O mesmo problema pode ser verificado na λ-expressao (λx . λz . xz)(λy . yz) que seria β-redutıvel a (λz . (λy . yz)z) e o z de (λy . yz) perderia seu contexto original, sendo capturado pelo

36

Page 45: LF Apostila

λz de (λx . λz . xz). Neste caso, uma possıvel sequencia de reducoes seria (λx . λz . xz)(λy . yz)α−→(λx . λa . xa)(λy . yz) β−→(λa . (λy . yz)a) η←→λy . yz.

Este problema tambem surge nas linguagens de programacao imperativas tradicionais, comoC e Pascal. Suponhamos que na declaracao de um procedimento se utilize uma variavel localdenominada x e que na execucao deste mesmo procedimento ele receba como parametro realuma variavel tambem denominada x que foi declarada fora do procedimento, portanto nao local,em relacao a ele. Uma referencia a variavel nao local x sera feita, na realidade, a variavel xlocal, ou seja a variavel x externa foi capturada pela variavel x interna. Isto acontece porque asvariaveis tem o mesmo nome (sao homonimas), apesar de representarem entidades distintas.

Exemplos resolvidos.

1. Faca a sequencia de reducoes para (λx . λy . + x((λx . − x 3)y))5 6.

(λx . λy . + x((λx . − x 3)y))5 6β−→(λy . + 5((λx . − x 3)y))6β−→+ 5 ((λx . − x 3)6)β−→+ 5 (− 6 3)

= + 5 3= 8

2. Deve ser verificado que as funcoes embutidas podem ser construıdas como quaisquer outrasfuncoes. Por exemplo,CONS = (λa . λb . λf . f a b),HEAD = (λc . c(λa . λb . a)) eTAIL = (λc . c(λa . λb . b)).

Vejamos agora uma sequencia de reducoes envolvendo estas duas funcoes:HEAD (CONS p q) = (λc . c(λa . λb . a)) (CONS p q)

β−→(CONS p q) (λa . λb . a)= (λa . λb . λf . f a b) p q ( λa . λb . a)

β−→(λb . λf . f p b) q (λa . λb . a)β−→(λf . f p q)(λa . λb . a)β−→(λa . λb . a) p qβ−→(λb . p) qβ−→p

Isto implica que nao ha a necessidade de que os construtores HEAD, CONS, TAIL ou umafuncao qualquer sejam pre-definidas. Na realidade, eles existem apenas por questao de eficiencia.

2.9 Conversao, reducao e abstracao

Podemos usar β-reducao no sentido oposto, para encontrar uma nova λ-abstracao. Por exemplo,+ 4 1 ← (λx . + x 1)4. A isto chamamos de β-abstracao. Uma β-conversao e um termo generico

utilizado tanto para uma β-reducao quanto para uma β-abstracao, simbolizando-se porβ←→. Por

exemplo, para este caso, + 4 1 β←→(λx . + x 1)4.

Resumo

1. Troca de nomes: α-conversao permite trocar o nome de um parametro de forma consis-tente.

37

Page 46: LF Apostila

2. Aplicacao de funcao: β-conversao permite aplicar uma λ-abstracao a um argumento,construindo uma nova instancia do corpo da λ-abstracao.

3. Eliminacao de λ-abstracoes redundantes: η-reducao pode, algumas vezes, eliminarλ-abstracoes redundantes.

2.10 Provando a conversibilidade

Muito frequentemente, nos deparamos com casos nos quais temos que provar a conversibilidadeentre duas λ-abstracoes. Quando as duas λ-expressoes denotam a mesma funcao, o mecanismode prova pode se tornar muito complicado e tedioso. Por exemplo, sejam as λ-expressoes IFTRUE ((λp . p) 3) e (λx . 3). Ambas denotam a mesma funcao, ou seja, a funcao que sempreretorna o valor 3, independente dos valores dos argumentos reais. Assim, espera-se que elassejam conversıveis uma na outra, ja que denotam a mesma funcao. Realizando as λ-conversoessobre a primeira, temos:

IF TRUE ((λp . p) 3)β←→IF TRUE 3η←→(λx . IF TRUE 3 x)

= (λx . 3) - pela definicao de IF.

Um metodo alternativo de se provar a convertibilidade de duas λ-expressoes que denotam amesma funcao, e que normalmente e mais conveniente, consiste em aplicar ambas as λ-expressoesa um mesmo argumento arbitrario, por exemplo, w. Vejamos como fica a prova de convertibili-dade das duas funcoes anteriores.

Tabela 2.2: Exemplo de aplicacoes ao mesmo argumento.

IF TRUE ((λp . p) 3) w (λx . 3) w

→ (λp . p) 3 pela def de IFβ−→3

β−→3

Portanto, IF TRUE ((λp . p)3) ↔ (λx . 3)

Este esquema de prova tem a vantagem de usar apenas reducao e evitar o uso explıcito deη-reducoes.

2.11 Uma nova notacao

A aplicacao das regras de conversao nem sempre e tao simples e direta e, por isso mesmo,sera mostrada uma definicao formal do que exatamente elas representam. Para isto, vamosintroduzir uma nova notacao que e bastante utilizada em exercıcios de conversao de redexes, porser intuitiva para a implementacao de redutores computacionalmente.

A notacao E[M/x] significa que, na expressao E, todas as ocorrencias livres de x seraosubstituıdas por M. Esta notacao nos permite expressar β-conversoes bem mais formalmente,sendo considerada mais natural por alguns implementadores.

Em uma aplicacao, a notacao se torna: (λx . E) Mβ−→E[M/x] que tambem pode ser utilizada

em α-conversoes. A Tabela 2.3 mostra um resumo desta notacao.

38

Page 47: LF Apostila

Tabela 2.3: Resumo das conversoes.x[M/x] = Mc[M/x] = c, onde c e uma constante distinta de x(E F) [M/x] = E [M/x] F[M/x](λx . E) [M/x] = λx . E(λy . E) [M/x] onde y e qualquer variavel distinta de x

= λy . E[M/x], se x nao ocorre livre em E OU y nao ocorre livre em M= λz . (E[z/y]) [M/x], onde z e o nome de uma nova variavel que nao

ocorre livre em E ou em M.Definicoes:α-conversao: se y nao e livre em E, entao (λx . E) α←→(λy . E[y/x])

β-conversao: (λx . E) Mβ←→E[M/x]

η-conversao: se x nao e livre em E e E denota uma funcao, entao (λx . E x) η←→E

2.12 Ordem de reducao

Um redex (reduction expression) e uma λ-expressao na qual todos os parametros necessariospara que uma operacao possa ser feita estao prontos para serem utilizados. Se uma expressaonao contiver qualquer redex a sua avaliacao esta completa e, neste caso, diz-se que a expressaoesta na sua forma normal.

No entanto, pode acontecer que uma expressao tenha mais de um textitredex e, neste caso,terıamos mais de um caminho a ser seguido para percorrer a sequencia de avaliacoes. Verificamosque, utilizando-se qualquer uma das sequencias de reducao, o resultado seria o mesmo. Porexemplo, + (* 3 4) (* 7 8) apresenta dois redexes. Iniciando a avaliacao da esquerda para adireita temos a seguinte sequencia de avaliacao:

+ (* 3 4) (* 7 8)−→ + 12 (* 7 8)−→ + 12 56−→ 68

Se, no entanto, fizermos a avaliacao da direita para a esquerda, sequencia de reducao sera:

+ (* 3 4) (* 7 8)−→ + (* 3 4) 56−→ + 12 56−→ 68

Verificamos que o resultado das duas sequencias de avaliacao e exatamente o mesmo. Noentanto, algumas observacoes devem ser feitas:

1. nem toda λ-expressao tem uma forma normal. Por exemplo, a λ-expressao (∆∆), onde ∆= (λx . x x) tem a seguinte sequencia de reducoes:

(λx . x x) (λx . x x) β−→(λx . x x) (λx . x x) β−→(λx . x x) (λx . x x) β−→. . .

correspondendo a um loop infinito nas linguagens imperativas.

2. Algumas sequencias de reducao podem atingir a sua forma normal enquanto outras nao.Por exemplo, (λx . 3) (∆∆) pode ser avaliada para 3, usando-se o primeiro redex, mas, seescolhermos o segundo (∆ aplicado a ∆), entraremos em loop infinito.

39

Page 48: LF Apostila

No entanto, ao perguntarmos se sequencias diferentes de reducao podem levar a formasnormais tambem diferentes, a resposta e NAO. Esta resposta incisiva e justificada por doisteoremas que serao descritos a seguir, apesar de nao exibirmos suas demonstracoes, que saodeixadas para consulta por alguem mais interessado e curioso sobre o tema, dado o caraterintrodutorio desta Apostila, e estas demonstracoes requerem conhecimentos avancados sobre ateoria da computacao.

Teorema 1 de Churh-Rosser (CRT-I).“Se E1 ←→ E2, entao existe uma expressao E talque E1 −→ E e E2 −→ E”.

Corolario. Nenhuma expressao pode ser convertida a duas formas normais distintas.

Dito de uma maneira informal, todas as sequencias de reducoes que terminam, chegarao aomesmo resultado, ou seja, a forma normal, se existir, e unica.

O teorema de Church-Rosser e conhecido como teorema da atingibilidade e unicidade daforma normal e tem uma longa historia: ele foi primeiramente demonstrado por Alonzo Churche J. Barkley Rosser em 1936 [24]. Esta demonstracao era tao longa e complicada que muito dotrabalho de pesquisa posterior foi feito tentando descobrir formas mais simples de demonstracaodeste teorema. Assim, o teorema ja foi demonstrado de varias formas para diferentes propositos,mas, todas elas apresentam alguma dificuldade. W. Tait, P. Martin-Lof e outros pesquisado-res iniciaram um sistema de demonstracao do teorema a partir de 1972. Uma demonstracaomais simples foi apresentada pelo prorio J. Barkley Rosser em 1982, tambem apresentada emMacLennan [24], e uma versao mais rigorosa foi feita por Barendregt em 1984.

O esquema de prova exibido em Maclennan se baseia na descricao de uma propriedade,conhecida como propriedade do diamante. Diz-se que uma relacao R tem a propriedade dodiamante se e somente se, para todas as formulas bem formadas X, X1 e X2 vale: se X R X1

e X R X2, entao existe uma formula bem formada X tal que X1 R X e X2 R X.

A partir da propriedade do diamante, o teorema de Church-Rosser e descrito da seguinteforma: “a reducao tem a propriedade do diamante”.

Teorema 2 de Church-Rosser (CRT II). “Se E1 −→ E2 e E2 esta na forma normal,entao existe uma ordem normal de sequencias de reducao de E1 para E2”.

Este teorema e tambem conhecido como “teorema da normalizacao” e significa que existe,no maximo, um resultado e a ordem normal o encontra, se ele existir. A ordem normal dereducao especifica que o redex mais a esquerda e mais externo - conhecido na literatura comoleftmost-outermost - deve ser realizado primeiramente. Dito de outra forma, “a reducao doredex mais externo e mais a esquerda, em cada ponto da sequencia de reducoes,nos leva ate a forma normal, se ela existir .”

Exemplo

Utilize a ordem normal de reducao para determinar a forma normal da expressao (λx .xx)((λy . y)(λz . z)).

(λx . xx)((λy . y)(λz . z))β−→((λy . y)(λz . z))((λy . y)(λz . z))β−→(λz . z)((λy . y)(λz . z))β−→(λy . y)(λz . z)β−→λz . z

2.13 Funcoes recursivas

A ideia de escolher o λ-calculo como linguagem intermediaria entre as linguagens funcionais,de alto nıvel, e a linguagem de maquina significa que todas as funcoes devem ser traduzidas

40

Page 49: LF Apostila

para ele. Na realidade, os programas funcionais representam apenas uma forma mais adequadade λ-expressoes. Dizemos que os programas funcionais sao “acucaramentos sintaticos” de λ-expressoes do λ-calculo, para torna-lo mais adequado e mais facil de ser tratado.

No entanto existe um problema a ser resolvido que se refere ao fato de uma das caracterısticasnotaveis dos programas funcionais e a utilizacao massiva de funcoes recursivas [29] e estas funcoesnao tem correspondentes no λ-calculo, conforme ja foi visto ate aqui. Isto acontece porque, noλ-calculo, as funcoes sao anonimas e, portanto, nao podem ser chamadas recursivamente.

Assim, torna-se necessaria uma forma de se implementar funcoes recursivas no λ-calculo.Nesta secao, vamos mostrar como estas funcoes sao traduzidas para o λ-calculo, sem a necessi-dade de qualquer extensao.

Voltemos, momentaneamente, nossa atencao para a definicao matematica da funcao f(x) =x3−x. Na realidade, estamos procurando valores para x e os respectivos resultados da aplicacaoda funcao fa estes valores. Como um caso particular, vamos procurar valores x que satisfacama igualdade f(x) = x, ou seja, estaremos procurando valores de x para os quais x3 − x = x. Umtal valor e x = 0, porque f(0) = 0. Mas x = ±21/2 tambem sao outros valores que satisfazem aigualdade f(x) = x.

Estes valores sao chamados de pontos fixos e representam uma grande fonte de estudo ma-tematico, destacando-se o teorema do ponto fixo, alem de outros. Os pontos fixos apresentamcaracterısticas importantes, no entanto o nosso interesse aqui se prende exclusivamente em uti-liza-los na construcao de funcoes recursivas no λ-calculo.

Considere agora a seguinte definicao recursiva da funcao fatorial:

FAT = (λn . IF (= n 0) 1 (* n (FAT (- n 1)))).

Nesta definicao, damos um nome a uma λ-abstracao (FAT ) e nos referimos a ele mesmo,dentro da λ-abstracao. Este tipo de construtor nao e provido pelo λ-calculo porque as λ-abstracoes sao funcoes anonimas e, portanto, elas nao podem fazer referencias a nomes.

Vamos colocar a λ-expressao FAT em uma forma mais adequada ao nosso desenvolvimento.Teremos entao FAT = λn . (. . .FAT. . .), onde os pontos representam as outras par-tes da funcao que nao nos interessam, neste momento. Fazendo uma β-abstracao em FAT,transformamo-la em FAT = (λfat . (λn . (. . .fat. . .))) FAT.

Esta funcao pode ser escrita na forma FAT = H FAT onde, H = (λfat . (λn . (. . .fat. ..))). Esta definicao de H e adequada aos nossos propositos, uma vez que ela e uma λ-abstracaoordinaria e nao usa recursao. A equacao FAT = H FAT estabelece que quando a funcao H eaplicada a FAT o resultado e o proprio FAT. Entao, FAT e um ponto fixo de H.

Vamos agora procurar um ponto fixo para H. Para isto vamos criar uma funcao, Y , quetoma H como argumento e retorna um ponto fixo da funcao, como resultado. Assim Y deve sertal que Y H seja um ponto fixo de H. Portanto, H(YH) = YH. Por este motivo, Y e chamadode combinador de ponto fixo. Se formos capazes de produzir tal combinador, nosso problemaestara resolvido.

Agora definimos FAT = Y H. Esta definicao nao e recursiva e atende as nossas necessidades.Para verificar isto, vamos computar (FAT 1) utilizando as definicoes de FAT e de H, dadasanteriormente e que o mecanismo de calculo obedece a ordem normal de reducao ou seja, leftmost-outermost.

FAT = Y HH = λfat . λn . IF (= n 0) 1 (* n(fat (- n 1))).

Entao

41

Page 50: LF Apostila

FAT 1 = YH 1= H (Y H) 1= (λfat . λn . IF (= n 0) 1 (* n(fat (- n 1))) (Y H) 1

β−→(λn . IF (= n 0) 1 (* n ((Y H) (- n 1)))) 1β−→(IF (= 1 0) 1 (* 1 ((Y H)(- 1 1))))

= (* 1 ((Y H) (- 1 1))) –pela definicao de IF= (* 1 ((H (Y H)) (- 1 1))) –pelo fato de YH ser um ponto fixo de H= (* 1 ((λfat . λn . IF (= n 0) 1 (* n (fat (- n 1)))) (Y H) (- 1 1)))

β−→ (* 1 (λn . IF (= n 0) 1 (* n (Y H (- n 1)))) (- 1 1))β−→(* 1 (IF (= (- 1 1) 0) 1 (* (- 1 1) (Y H (- (- 1 1) 1)))))

= (* 1 (IF (= 0 0) 1 (* 0 (Y H (- 0 1)))))= (* 1 (IF TRUE 1 (* 0 (Y H (- 0 1)))))= (* 1 1) –pela definicao de IF−→ 1

A forma como o combinador Y e definido ja foi mostrada anteriormente, no entanto ele seranovamente aqui definido, usando apenas algumas renomeacoes de variaveis:

Y = λh . (λx . h (x x)) (λx . h (x x)).

Vamos agora avaliar Y H.

YH = (λh . (λx . h (x x)) (λx . h (x x))) Hβ←→(λx . H (x x)) (λx . H (x x))β←→H ((λx . H (x x)) (λx . H (x x)))

= H (YH)

Este resultado confirma o fato de que Y H e um ponto fixo de H ou seja, o combinador Y ,quando aplicado a uma funcao, retorna um ponto fixo desta funcao.

2.14 Resumo

Este Capıtulo versou sobre os primeiros passos para quem necessita conhecer o λ-calculo deforma simples e introdutoria. Ele se fez necessario dada a importancia que esta teoria temna fundamentacao das linguagens funcionais. Seu papel e semelhante ao desempenhado pelalinguagem Assembly na traducao das linguagens imperativas para o codigo de maquina. Noentanto, deve ser salientado que esta teoria da matematica nao e tao simples como aqui parece.Esta abordagem simples foi adotada, dado o carater introdutorio exigido para se entender comoo λ-calculo e utilizado na compilacao de linguagens funcionais. Quem quizer se aprofundar nestetema deve consultar a bibliografia indicada.

As notas de aula de Rafael Lins [22] e de Peter Welch [38] representam um bom comeco,dada a grande quantidade de exercıcios indicados e resolvidos. Quem estiver interessado emdetalhes mais aprofundados sobre a implementacao do λ-calculo deve consultar o livro de PeytonJones [29] que apresenta o λ-calculo de forma adequada para quem quer entender detalhes deimplementacao. Para uma abordagem mais teorica desta linguagem, deve-se consultar os livrosde Bruce MacLennan [24] e de Antony Davie [7].

42

Page 51: LF Apostila

Capıtulo 3

Programacao funcional em Haskell

”There are many distint pleasuresassociated with Computer programming.

Craftsmanship has its quiet rewards, the satisfaction thatcomes from building a useful object and making it work.”

(Steven S. Skiena et Miguel A. Revilla in [33])

3.1 Introducao

Os Capıtulos anteriores foram feitos com o objetivo de servirem como preparacao e funda-mentacao para o estudo das linguagens funcionais, em particular, de Haskell. Este Capıtulo e osseguintes sao todos dedicados a codificacao de programas funcionais utilizando esta linguagem.A comunidade das linguagens funcionais tem dado a Haskell uma atencao especial e, por estemotivo, muita pesquisa tem sido feita tentando dota-la de caracterısticas que a torne uma lin-guagem de uso popular. Estas caracterısticas foram citadas por Philip Wadler [37] e analisadasna Introducao desta Apostila.

Haskell e uma linguagem funcional pura, nao estrita, fortemente tipada, cujo nome e umahomenagem a Haskell Brooks Curry, um estudioso da logica combinatorial e um dos mais pro-eminentes pesquisadores sobre λ-calculo [4, 35]. E uma linguagem baseada em scripts, queconsistem em um conjunto de definicoes associadas a nomes, em um arquivo.

Em 1998, a comunidade de Haskell padronizou Haskell98 como a versao a ser utilizada atea definicao de Standard Haskell. No entanto, a linguagem continua sendo pesquisada buscandoa criacao de novas Bibliotecas a serem incorporadas ao sistema. Tambem muitas extensoesestao sendo incorporadas, como Haskell paralelo, IDLs (Interface Description Language), porexemplo HaskellDirect e interfaces para C e C++, permitindo integracao com estas e outraslinguagens. Em particular, tem sido desenvolvida AspectH, uma extensao de Haskell parasuportar orientacao a aspectos [2], alem de uma extensao para OpenGL.

O site oficial na WEB sobre Haskell e: http://www.haskell.org, onde todas as informacoessobre ela podem ser encontradas, alem de varios links para compiladores e interpretadores paraa linguagem.

O interpretador mais popular de Haskell e Hugs, desenvolvido por Mark Jones na Uni-versidade de Nottingham e da Universidade de Yale. Esta implementacao, codificada em C,e pequena, facil de ser usada e disponıvel para varias plataformas, incluindo UNIX, Linux,Windows 3.x, Win32, DOS e ambiente Macintosh.

Para produzir codigo executavel de maquina, foram desenvolvidos varios compiladores. NaUniversidade de Glasgow, foi construıdo GHC (Glasgow Haskell Compiler) disponıvel para

43

Page 52: LF Apostila

ambientes UNIX (Linux, Solaris, *BSD e MacOS-X) e tambem para Windows. E consideradoum pouco lento e necessita de muita memoria. Esta disponıvel de forma livre em

http://www.dcs.gla.ac.uk/fp/software/ghc/

Na Universidade de Chalmers, foram desenvolvidos o interpretador HBI (Haskell-B Inter-preter) e o compilador HBC (Haskell-B Compiler) disponıveis em

http://www.cs.chalmers.se/ augustss/hbc.html.

Tambem esta disponıvel o compilador nhc98, considerado facil de ser instalado, com heapprofiles, muito menor que os outros compiladores, disponıvel para todos os padroes UNIX eescrito em Haskell 98.

Existe ainda uma linguagem e um compilador, Helium, dedicados ao ensino de Haskell. Eum subconjunto de Haskell, onde a principal diferenca e a ausencia de sobrecarga.

3.2 Primeiros passos

Existem duas formas nas quais um texto e considerado um programa em Haskell. A primeiradelas e considerar todo o texto como um programa, exceto o que e comentado, que pode serde duas maneiras: com “- -“, que representa um comentario ate o final da linha corrente, ouenvolvendo o comentario com os sımbolos “{-” e “-}”, podendo englobar varias linhas. Osarquivos em Haskell com este tipo de programa devem ter a extensao .hs. Esta e a maneira maisutilizada pelos programadores de Haskell.

A outra forma e considerar todo o texto como comentario e sinalizar o que deve ser realmenteum programa iniciando a linha com o sinal “>” identado. Neste caso, o arquivo deve ter extensao.lhs. Vamos mostrar um exemplo de cada situacao.

{- ######################################################################exemplo.hsEste arquivo eh um exemplo de um arquivo .hs. Deve ser editado como arquivotexto e salvo com a extensao .hs.#########################################################################-}

resposta :: Int -- Uma constante inteiraresposta = 42

novalinha :: Charnovalinha = ’\n’

sim :: Boolsim = True

maior :: Boolmaior = (resposta > 71)

quadrado :: Int -> Intquadrado x = x*x

todosIguais :: Int -> Int -> Int -> BooltodosIguais n m p = (n ==m) && (m ==p)

{-######################################################################-}

Agora vamos mostrar o mesmo exemplo usando a visao de literal, com a extensao .lhs. Deve

44

Page 53: LF Apostila

ser observado que os sinais de inıcio e fim de comentarios desaparecem.

##########################################################################exemplo.lhs . Este arquivo eh um exemplo de um arquivo .lhs.##########################################################################

> resposta :: Int -- Uma constante inteira> resposta = 42

> novalinha :: Char> novalinha = ’\n’

> sim :: Bool> sim = True

> maior :: Bool> maior = (resposta > 71)

> quadrado :: Int -> Int> quadrado x = x*x

> todosIguais :: Int -> Int -> Int -> Bool> todosIguais n m p = (n ==m) && (m ==p)

#########################################################################

3.2.1 O interpretador Hugs

O interpretador Hugs disponibiliza a biblioteca de funcoes pre-definidas que compoe o arquivo“Prelude.hs”, que podem ser utilizadas pelo usuario a partir do instante em que o interpretadore carregado. Na chamada, e aberta uma secao, que permanece ativa enquanto o sistema estiverem execucao.

Os comandos de Hugs sao muito simples e nao oferecem muitas possibilidades ao usuario.Eles podem ser vistos pela chamada ao help, atraves do comando :?. Alguns deles podem serobservados na Tabela 3.1.

Tabela 3.1: Principais comandos de Hugs.Comando Acao realizada:? Aciona o help:e Chama o script atual:e exemplo.hs Edita o arquivo exemplo.hs:l exemplo.hs Carrega o script exemplo.hs e limpa outros arquivos carregados:a exemplo.hs Carrega o script exemplo.hs sem limpar os outros arquivos:q Termina a secao

Para o usuario executar qualquer funcao do Prelude e necessario apenas chamar esta funcaona linha de comandos (prompt), disponıvel apos o interpretador ser carregado, seguida de seusargumentos e apertar a tecla “Enter”. O resultado sera exibido imediatamente na linha seguinteapos o “prompt”. O interpretador tambem pode ser usado como uma calculadora para avaliarexpressoes aritmeticas, booleanas, logarıtmicas, trigonometricas, etc. A forma de utilizacao e

45

Page 54: LF Apostila

apenas colocar a expressao no prompt e apertar “Enter”. Caso a funcao ou expressao a serexecutada esteja com seus parametros colocados corretamente, o resultado aparecera na linhaseguinte. Caso a chamada da funcao ou expressao nao esteja correta, sera exibida uma mensagemde erro. Algumas destas funcoes ou operadores aritmeticos estao mostrados na Tabela 3.2.

Tabela 3.2: Tabela dos operadores de Haskell, com suas prioridades.Prioridade Assoc. a esquerda Nao associativa Assoc. a direita9 !, !!, //, > . > >>= .8 **, ˆ, ˆˆ7 % , /, ‘div‘, ‘mod‘, ‘rem‘, ‘quot‘6 +, - :+5 \\ :, ++, > + >

4 /=, <, <=, = =, >, >=,‘elem‘, ‘notElem‘

3 &&2 ||1 :=0 $

Os operadores podem ser infixos ou pre-fixos. Normalmente os operadores aritmeticos saodeclarados como infixos, por exemplo, se usa a expressao ‘3 + 7’, por ser esta a forma comumenteutilizada na Matematica. Ja as funcoes sao normalmente declaradas como pre-fixas por ser aforma mais utilizada nas demais linguagens de programacao. No entanto, os operadores infixospodem ser utilizados como pre-fixos, apenas colocando o operador entre parenteses. Por exemplo,o operador ‘+’ (infixo) pode ser aplicado como (+) 2 3 (pre-fixo). Os operadores pre-fixostambem podem ser aplicados como infixos, apenas colocando o operador entre aspas simples (oacento agudo em ambos os lados do operador). Por exemplo, ‘maxi 3 4’ (pre-fixo) pode serutilizado como 3 ’maxi’ 4 (infixo).

E possıvel tambem trocar a associatividade ou a prioridade de um operador. Para isso,e necessario declarar explicitamente o tipo de associatividade e a prioridade da funcao. Porexemplo, para trocar a associatividade e a prioridade da funcao toma, pode-se fazer a declaracao:

Infixl 7 toma

significando que a funcao toma e infixa, associa-se pela esquerda e tem um nıvel de prioridade7. Se a prioridade for omitida, sera considerada igual a 9, por default. Seguem alguns exemplosde chamadas a calculadora de expressoes ou de funcoes.

Exemplos

:? 2 + 3 <enter>5

:? (1 * 6) == (3 ‘div‘ 5) <enter>False

:? sin(3 + 4) <enter>Error: Type mismatched --Por que esta mensagem de erro?

46

Page 55: LF Apostila

3.2.2 Identificadores em Haskell

Os identificadores em Haskell sao sensıveis a caracteres, ou seja, as letras maiusculas sao distintasdas letras minusculas. Os identificadores, devem ser iniciados, sempre, por uma letra maiuscula,se for um tipo, ou minuscula, se for um outro identificador como uma variavel, uma constanteou uma funcao. A esta primeira letra do identificador podem ser seguidos outros caracteres, quepodem ser letras maiusculas ou minusculas, dıgitos, sublinhado ou acentos agudos. Por exemplo,

type Par = (Int, Int)somaAmbos :: Par -> IntsomaAmbos (primeiro, segundo) = primeiro + segundo

Deve ser lembrado aqui que Haskell e uma linguagem funcional pura e, como tal, nao per-mite atribuicoes destrutivas, ou seja, nao e possıvel fazer atualizacoes de variaveis em Haskell.Isto significa que, nos exemlos mostrados anteriormente, a variavel resposta tera o valor 42enquanto o script estiver ativo. Se for necessario atribuir outro valor para resposta, tera queser criada uma outra variavel para isto. Desta forma, em Haskell, as variaveis sao consideradassao consideradas constantes, uma vez que elas nao podem ser atualizadas.

As palavras reservadas da linguagem sao sempre escritas em letras minusculas. Haskellapresenta 22 palavras reservadas, mostradas a seguir:

case else infix module typeclass hiding infixl of wheredata if infixr renamingdefault import instance thenderiving in let to

3.3 Funcoes em Haskell

As formas de definicao de funcoes em Haskell tem a ver com as formas de definicao de funcoesutilizadas na Matematica, mostradas no Capıtulo 1. Em Haskell, elas podem ser pensadas erepresentadas graficamente por uma caixa que recebe um ou mais parametros como entrada(argumentos), processa-os e constroi um resultado unico que e exibido como saıda, conformepode ser visto na Figura 3.1.

IntInt

Int+

QuadroInt

Quadroescala

Figura 3.1: Representacao grafica das funcoes + e escala.

Exemplos de funcoes:

• Uma funcao que calcula as raızes de uma equacao bi-quadrada.

• Uma funcao que emite o relatorio final dos resultados com as notas parciais e final dosalunos da disciplina Topicos em Linguagem de Programacao.

47

Page 56: LF Apostila

• Uma funcao que controla a velocidade de um automovel.

Mais exemplos de funcoes podem ser encontrados em qualquer atividade da vida. Na Figura3.2 estao mostradas graficamente algumas funcoes baseadas no livro de Simon Thompson [35].Na Figura, cada desenho do lado esquerdo ou do lado direito e um “Quadro”. Um Quadro eo elemento de entrada da funcao que o processa e transforma em outro Quadro. Por exemplo afuncao espelhaV toma como argumento um Quadro e faz o espelhamento deste Quadro emrelacao ao eixo vertical do plano xy.

Ate este ponto nao foi mostrada uma forma como cada Quadro pode ser implementado,para ser simulado e processado por um programa. Isto aconteceu porque o objetivo foi apenasmostrar exemplos de funcoes. A modelagem destes objetos serao feitas ao longo do texto.

espelhaV

espelhaH

invertecor

escala

sobrepoe

Figura 3.2: Resultados graficos de funcoes.

No entanto, e necessario salientar a importancia que os tipos dos argumentos e dos resultadostem nas definicoes de funcoes. Eles permitem ao programador estabelecer uma correspondenciabem definida entre eles e os objetos que modelam, proporcionando uma simulacao adequada.Assim, as funcoes desta Figura tem os tipos:

espelhaV :: Quadro -> QuadroespelhaH :: Quadro -> Quadroinvertecor :: Quadro -> Quadroescala :: Quadro -> Quadrosobrepoe :: Quadro -> Quadro

48

Page 57: LF Apostila

3.3.1 Construindo funcoes

Um tipo de dado e uma colecao de valores onde todos eles tem as mesmas caracterısticas.Por exemplo, os numeros inteiros, os caracteres, os strings de caracteres, etc. As funcoes emHaskell podem ser declaradas com o seu nome, seguido de ::, vindo em seguida os tipos de seusargumentos, um a um, com uma flecha (− >) entre eles e, finalmente mais uma flecha seguidado tipo do resultado que a aplicacao da funcao produz. Por exemplo, as funcoes + e escala,mostradas graficamente na Figura 3.1, tem seus tipos definidos da forma a seguir:

+ :: Int -> Int -> Int eescala :: Quadro -> Int -> Quadro

O tipo da funcao e daclarado em sua forma “currificada”, onde uma funcao de n argumentose considerada como n funcoes de um unico argumento, da mesma forma adotada pelo λ-calculo,vista no Capıtulo 2.

Os tipos nos dao informacoes importantes sobre a aplicacao das funcoes. Por exemplo, adeclaracao da funcao escala, mostrada anteriormente, nos informa que:

• a funcao escala tem dois argumentos de entrada, sendo o primeiro do tipo Quadro e osegundo do tipo Int e

• o resultado da aplicacao da funcao e um valor do tipo Quadro, facilitando o entendimentodo problema e a forma de solucao adotada para resolve-lo.

Alem dos tipos, a declaracao de uma funcao deve exibir explicitamente como o processamentode seus argumentos deve ser feito ou pode declarar que este processamento seja feito atravesde definicoes ja feitas para outras funcoes, sendo esta uma forma muito comum de declaracao.Como exemplo do primeiro caso, podemos verificar a definicao da funcao somaAmbos. Aforma como ela processa um par de valores, (x, y), e somando seus valores, x + y.

Como exemplo de declaracao de uma funcao atraves de outras funcoes, temos a funcaorotaciona, que promove uma rotacao em torno da origem do sistema de coordenadas cartesianasx0y, pode ser definida em funcao das funcoes espelhaV e espelhaH, definidas anteriormente.

rotaciona :: Quadro -> Quadrorotaciona pic = espelhaH (espelhaV pic)

Algumas linguagens funcionais exigem que os tipos das funcoes sejam declarados explicita-mente pelo programador. Em Haskell, seus projetistas optaram por deixar que os tipos dasfuncoes sejam inferidos pelo sistema. Isto significa que e opcional a declaracao dos tipos dasfuncoes pelo programador. Mesmo assim, encoraja-se que esta declaracao seja feita explicita-mente, como forma de disciplina de programacao. Isto permite ao programador um completoentendimento do problema e da solucao adotada.

Voltando ao exemplo da funcao rotaciona, ela tambem pode ser codificada de outra forma,mais elegante, utilizando a composicao de funcoes, uma propriedade implementada apenas naslinguagens funcionais. Esta forma de construcao de funcoes sera vista com detalhes ainda nesteCapıtulo, no entanto, ela pode ser feita em Haskell usando a notacao de ponto (.), a mesmaadotada para a composicao de funcoes na Matematica.

rotaciona = espelhaH . espelha

49

Page 58: LF Apostila

Modelagem. Suponhamos que um cavalo seja um objeto do tipo Quadro (ja visto anterior-mente). Um Quadro pode ser modelado por uma matriz (12x12), de caracteres. Desta forma,um cavalo e uma lista de 12 linhas e cada linha e uma lista de 12 caracteres, ou seja, um cavaloe uma lista de listas de caracteres1, conforme pode ser visto graficamente na representacao aseguir, onde os pontos estao colocados apenas para facilitar a contagem, ou seja, eles estao colo-cados apenas para representar os caracteres em branco. Os resultados das aplicacoes das funcoesespelhaH e espelhaV a um cavalo tambem estao mostrados na mesma Figura, a seguir.

cavalo espelhaH cavalo espelhaV cavalo.......##... ......##.... ...##............##..#.. .....#.#.... ..#..##........##.....#. ....#..#.... .#.....##.....#.......#. ...#...#.... .#.......#....#...#...#. ..#...#..... .#...#...#....#...###.#. ..#...#..##. .#.###...#....#...#..##. ..#...###.#. .##..#...#....#...#..... ..#...#...#. .....#...#.....#...#.... ..#.......#. ....#...#.......#..#.... ...##.....#. ....#..#.........#.#.... .....##..#.. ....#.#...........##.... .......##... ....##......

Em Haskell, a declaracao do tipo de um Quadro e feita da seguinte forma:

type Linha = [Char]type Quadro = [Linha]

As funcoes espelhaH e espelhaV tambem podem ser declaradas a partir de outras funcoesja definidas para outras finalidades, o que proporciona ainda mais flexibilidade ao programador.Por exemplo,

espelhaH cav = reverse cav --inverte os elementos de uma listaespelhaV cav = map reverse cav

A funcao reverse, quando aplicada a uma lista de elementos de qualquer tipo da, comoresultado, uma outra lista com os mesmos elementos da primeira lista, na ordem inversa. Porexemplo, reverse [1,2,3] = [3,2,1] e reverse [’a’, ’b’, ’c’] = [’c’, ’b’, ’a’]. Isto significaque a funcao reverse aplicada a cada linha do cavalo, que e uma lista de caracteres, da, comoresultado, a mesma linha mas com a ordem de seus caracteres invertida. A funcao map tomaa funcao reverse, o primeiro de seus dois argumentos, e a aplica a cada uma das linhas de seusegundo argumento (um cavalo). As funcoes map e reverse sao pre-definidas em Haskell e elasserao objeto de estudo mais profundo nas proximas secoes.

Algumas conclusoes importantes podem ser tiradas a partir destes exemplos:

• a funcao reverse pode ser aplicada a uma lista de valores de qualquer tipo. Isto significaque uma mesma funcao pode ser aplicada a mais de um tipo de dados. Isto significapolimorfismo, ou seja, a utilizacao generica de uma funcao, aumentando a produtividadede software.

1O tipo lista e um tipo primitivo em Haskell (o tipo mais importante nas linguagens funcionais) e, dada a suaimportancia, sera dedicado, um Capıtulo completo (o Capıtulo 4) sobre a sua construcao e utilizacao.

50

Page 59: LF Apostila

• Um argumento da funcao map e reverse e reverse e tambem uma funcao. Isto significaque uma funcao (reverse) pode ser passada como parametro para uma outra funcao.Neste caso, diz-se que as funcoes sao cidadaos de primeira categoria, permitindo a elas osmesmos direitos que qualquer outro tipo de dado.

• O resultado da aplicacao da funcao map a funcao reverse e uma outra funcao que eaplicada a um outro argumento que, neste caso, e uma lista. Neste caso, diz-se que, naslinguagens funcionais, as funcoes sao de alta ordem, ou seja, podem ser passadas comoargumentos de uma funcao e tambem podem retornar como resultados da aplicacao deuma funcao.

3.3.2 Avaliacao de funcoes em Haskell

A forma de avaliacao de funcoes utilizada em programas codificados em Haskell e a ordem normalde avaliacao, preconizada pelo segundo teorema de Russell visto no Capıtulo 2. Isto significaque Haskell obedece ao sistema leftmost-outermost, usando um mecanismo de avaliacao lazy(preguicoso) que so avalia uma expressao se ela for realmente necessaria e no maximo uma vez.Isto significa um tipo de avaliacao semelhante a avaliacao curto-circuito utilizada em algumaslinguagens convencionais. Vejamos um exemplo de avaliacao usando as funcoes definidas nosscripts mostrados no inıcio deste Capıtulo.

todosIguais (quadrado 3) resposta (quadrado 2)= ((quadrado 3) == resposta) && (resposta == (quadrado 2))= ((3 * 3) == resposta) && (resposta == (quadrado 2))= (9 == resposta) && (resposta == (quadrado 2))= (9 == 42) && (42 == (quadrado 2))= False && (42 == (quadrado 2))= False (utilizando o mecanismo de avaliacao lazy)

3.3.3 Casamento de padroes (patterns matching)

O casamento de padroes e outra forma de codificacao de funcoes em Haskell, baseada na de-finicao por enumeracao utilizada na Matematica, vista no Capıtulo 1. Neste tipo de definicao,sao exibidos todos os valores que os argumentos da funcao podem ter e, para cada um deles,declara-se o valor do resultado correspondente. De forma resumida, exibem-se todos pares domapeamento (entrada, resultado). Por exemplo, vejamos as declaracoes das funcoes eZero efat, a seguir:

eZero :: Int -> Bool fat :: Int -> InteZero 0 = True e fat 0 = 1eZero _ = False fat n = n * fat (n --1)

Na execucao de aplicacoes destas funcoes, os padroes sao testados sequencialmente, de cimapara baixo. O primeiro padrao que casa com o valor da entrada tera o valor correspondentecomo resultado da aplicacao da funcao. Se nao acontecer quelquer casamento entre o valor deentrada e um padrao de entrada, o resultado da aplicacao da funcao sera um erro. Ao se aplicara funcao eZero a um argumento n, primeiro e verificado se este numero n casa com 0. Se forverdade, o resultado sera True. Se este padrao nao for verificado, ou seja, se n nao for iguala 0, verifica-se o casamento com o segundo padrao e assim por diante. Neste caso, o resultadosera False. O mesmo reciocınio vale para a funcao fat.

Esta forma de analise sequencial deve sempre ser levada em consideracao para que errosgrosseiros sejam evitados. Como exemplo, se, na declaracao da funcao eZero, a definicao da

51

Page 60: LF Apostila

funcao para o caso de n ser 0 for trocada pela segunda definicao, o resultado da aplicacao dafuncao sera sempre igual a False, mesmo que o argumento seja 0, uma vez que o (undescore)significa “qualquer caso”.

Exercıcios:

1. De a definicao da funcao todosQuatroIguais do tipo Int− >Int− >Int− >Int− >Boolque da o resultado True se seus quatro argumentos forem iguais.

2. De definicao da funcao todosQuatroIguais usando a definicao da funcao todosIguais,dada anteriormente.

3. O que esta errado com a definicao da funcao todosDiferentes abaixo?

todosDiferentes n m p = ( (n /= m) & & (m /= p) )

4. Projete um teste adequado para a funcao todosIguais, considerando a funcao

teste :: Int− >Int− >Int− >Intteste n m p = ((n+m+p) == 3*p).

Esta funcao se comporta da mesma forma que a funcao todosIguais para o seu teste dedados? Que conclusao voce tira sobre os testes em geral?

5. De uma definicao para a funcao quantosIguais usando as funcoes todosIguais e todos-Diferentes.

6. Escreva a sequencia de calculos para as seguintes expressoes:

maximo ((2 + 3) – 7) (4 + (1 – 3))todosIguais 4 quadrado 2 3quantosIguais 3 4 3

3.4 Tipos de dados em Haskell

Haskell, a exemplo de qualquer linguagem de programacao, prove uma colecao de tipos primitivose tambem permite tipos estruturados, definidos pelo programador, provendo grande flexibilidadena modelagem de programas.

3.4.1 Os tipos primitivos da linguagem

Os tipos primitivos de Haskell sao: o tipo inteiro (Int ou Integer), o tipo booleano (Bool), otipo caractere (Char), o tipo cadeia de caracteres (String) e o tipo ponto flutuante (Float ouDouble) e o tipo lista. Nesta secao, vamos analisar cada um destes tipos primitivos, deixandoo tipo lista para ser tratado no Capıtulo 4, dada a sua importancia nas linguagens funcionais e,em particular, em Haskell. Tambem serao estudados os tipos estruturados possıveis em Haskell.

O tipo inteiro (Int ou Integer)

Como em muitas linguagens, o tipo inteiro e primitivo em Haskell. Seu domınio de valorese o mesmo das outras linguagens. Os valores do tipo Integer sao representados com o dobroda quantidade de bits necessarios para representar os valores do tipo Int. Seus operadoresaritmeticos sao os mesmos admitidos na maioria das outras linguagens:

Operadores aritmeticos para valores dos tipos Int ou Integer.

52

Page 61: LF Apostila

+, * adicao e multiplicacaoˆ exponenciacao- subtracao (infixa) e inversor de sinal (prefixa)div divisao inteira (prefixa), ou ‘div‘ (infixa)mod modulo (prefixa), ou ‘mod‘ (infixa)abs valor absoluto de um inteironegate troca o sinal de um inteiro

Os operadores relacionais tambem sao os mesmos encontrados na grande maioria das lingua-gens.

Operadores relacionais: Int− >Int− >Bool>, >=, ==, / =, <=, <

Vejamos um exemplo simples de construcao de funcao usando inteiros.

mdc :: Int -> Int -> Intmdc n m

|m == n = n|m > n = mdc m n|otherwise = mdc (n - m) m

Exemplo. Vamos agora mostrar uma forma de construcao de um programa que envolveoperacoes com inteiros. Seja uma empresa que necessita de respostas para as questoes a se-guir, para fundamentar suas tomadas de decisoes:

• Questao 1: Qual o total de vendas desde a semana 0 ate a semana n?

• Questao 2: Qual a maior venda semanal entre as semanas 0 e n?

• Questao 3: Em que semana ocorreu a maior venda?

• Questao 4: Existe alguma semana na qual nada foi vendido?

• Questao 5: Em qual semana nao houve vendas? (se houve alguma).

Vamos construir algumas funcoes em Haskell para responder a algumas destas questoes,deixando algumas outras para exercıcio do leitor. Para isto e necessario que recordemos asdefinicoes matematicas de funcoes, vistas no Capıtulo 1.

Questao 1:

Para solucionar a Questao 1, devemos inicialmente construir uma funcao vendas i quevai nos informar qual o valor das vendas na semana i. Esta funcao pode ser construıda devarias formas, no entanto sera feita aqui uma definicao por casamento de padroes, semelhantea definicao por enumeracao da Matematica.

vendas :: Int -> Intvendas 0 = 7vendas 1 = 2vendas 2 = 5

Verifiquemos agora como as definicoes recursivas utilizadas na Matematica podem ser uti-lizadas em Haskell. Recordemos a definicao matematica de uma funcao usando recursao. Porexemplo, na definicao de uma funcao fun, devemos:

53

Page 62: LF Apostila

• explicitar o valor de fun 0, (caso base)

• explicitar o valor de fun n, usando o valor de fun (n - 1) (caso recursivo ou passoindutivo).

Trazendo esta forma de definicao para o nosso caso, vamos construir a funcao totaldeVendasi a partir de vendas i, que vai mostrar o total de vendas realizadas ate a semana i, inclusive.

totaldeVendas :: Int->InttotaldeVendas n

|n == 0 = vendas 0|otherwise = totaldeVendas (n-1) + vendas n

Neste caso, em vez de usarmos padroes, serao usadas equacoes condicionais (booleanas),tambem chamadas de “guardas”, cujos resultados sao valores True ou False. As guardas saoavaliadas tambem sequencialmente, da mesma forma feita com o casamento de padroes. Apalavra reservada otherwise exerce um papel parecido, mas diferente, do (sublinhado), jadescrito anteriormente. A clausula otherwise deve ser utilizada apenas quando houver guardasanteriores, cujos resultados sejam todos False. Isto significa que se a clausula otherwise forcolocada na primeira definicao de uma funcao, ocorrera um erro, uma vez que nao existemguardas definidas anteriormente. No caso do (sublinhado), esta situacao nao ocasionara erro.

Usando a definicao de vendas i e de totaldeVendas i, dadas anteriormente, podemoscalcular a aplicacao da funcao totaldeVendas 2, da seguinte forma:

totaldeVendas 2 = totaldeVendas 1 + vendas 2= (totaldeVendas 0 + vendas 1) + vendas 2= (vendas 0 + vendas 1) + vendas 2= (7 + vendas 1) + vendas 2= (7 + 2) + vendas 2= 9 + vendas 2= 9 + 5= 14

Questao 2:

Vamos agora definir a funcao maxVendas, onde maxVendas i sera igual a vendas i,se a venda maxima ocorreu na semana i. Se a maior venda ocorreu na semana 0, a funcaomaxVendas 0 sera vendas 0. Se a maior venda ocorreu ate a semana n, pode estar ate asemana (n-1) ou sera vendas n.

maxVendas :: Int -> IntmaxVendas n

|n == 0 = vendas 0|maxVendas (n-1) >= vendas n = maxVendas (n - 1)|otherwise = vendas n

Esta mesma funcao pode ser construıda de outra forma, usando uma funcao auxiliar, ma-ximo, que aplicada a dois inteiros retorna o maior entre eles.

maximo :: Int -> Int -> Intmaximo x y

54

Page 63: LF Apostila

|x >= y = x|otherwise = y

maxVendas n|n == 0 = vendas 0|otherwise = maximo (maxVendas (n - 1)) vendas n

Esta forma de definicao de funcoes e chamada recursao primitiva.

E se chamarmos totaldeVendas (-2)? O resultado sera =⇒ ERROR: Control stackoverflow, uma vez que a pilha do sistema vai estourar. Esta entrada caracteriza uma excecaoe, como tal, deve ser tratada. Em Haskell, existem varias formas de tratar excecoes, mas elasserao vistas no Capıtulo 6. Apenas para exemplificar, uma forma de fazer isto e definir um valorfictıcio para o caso de n ser negativo, transformando a definicao dada na seguinte:

totaldeVendas n|n == 0 = vendas 0|n > 0 = totaldeVendas (n - 1) + vendas n|otherwise = 0

De forma resumida, ate agora vimos tres formas de definir funcoes:

1. quadrado x = x * x

2. maximo n m| n >= m = n (equacao condicional)| otherwise = m

3. usar o valor da funcao sobre um valor menor (n - 1) e definir para n.

Exercıcios

1. Defina uma funcao para encontrar a semana em que ocorreu a venda maxima entre asemana 0 e a semana n. O que sua funcao faz se houver mais de uma semana com vendasmaximas?

2. Defina uma funcao para encontrar uma semana sem vendas entre as semanas 0 e n. Senao existir tal semana, o resultado deve ser n + 1.

3. Defina uma funcao que retorne o numero de semanas sem vendas (se houver alguma).

4. Defina uma funcao que retorna o numero de semanas nas quais foram vendidas s unidades,para um inteiro s ≥ 0. Como voce usaria esta solucao para resolver o problema 3?

5. Teste as funcoes que usam vendas com a definicao vendas n = n ‘mod‘ 2 + (n + 1) ‘mod‘3

6. De uma definicao da funcao fat que calcula o fatorial de n, onde n e um inteiro positivo.

7. De uma definicao de uma funcao de m e n que retorna o produto

m * (m + 1) * ... *(n – 1) * n.

8. De uma definicao de uma funcao que retorne i-esimo numero da sequencia de Fibonacci(0, 1, 1, 2...).

55

Page 64: LF Apostila

O tipo booleano (Bool)

Os unicos valores booleanos sao True e False e sobre eles podem ser utilizadas funcoespre-definidas ou funcoes construıdas pelo usuario. As funcoes pre-definidas sao:

Funcao Nome Tipo&& and && :: Bool − > Bool − > Bool|| or || :: Bool − > Bool − > Boolnot inversor not :: Bool − > Bool

A funcao OU exclusivo pode ser definida pelo usuario da seguinte forma:

exOr :: Bool -> Bool -> BoolexOr True x = not xexOr False x = x

Exercıcios

1. De a definicao de uma funcao nAnd :: Bool − > Bool − > Bool que da o resultadoTrue, exceto quando seus dois argumentos sao ambos True.

2. Defina uma funcao numEquallMax :: Int − > Int − > Int − > Int onde numE-quallMax n m p retorna a quantidade de numeros iguais ao maximo entre n, m ep.

3. Como voce simplificaria a funcao

funny x y z|x > z = True|y >= x = False|otherwise = True

O tipo caractere (Char)

Os caracteres em Haskell sao literais escritos entre aspas simples. Existem alguns caracteresque sao especiais por terem utilizacoes especıficas. Entre eles se encontram:

‘\t’ - tabulacao ‘\” - aspas simples‘\n’ - nova linha ‘\”’ - aspas duplas‘\\’ - uma barra invertida ‘\34’ - ?

Existem algumas funcoes pre-definidas em Haskell feitas para converter caracteres em numerose vice-versa.

toEnum :: Int -> CharfromEnum :: Char -> Int

O tipo cadeia de caracteres (String)

O tipo cadeia de caracteres, normalmente chamado de String, tem uma caracterıstica pecu-liar em Haskell. Apesar deste caso ser considerado um caso patologico por alguns pesquisadoresde linguagens de programacao, os criadores de Haskell admitiram duas formas para este tipo.Ele e um tipo pre-definido como String, mas tambem pode ser considerado como uma lista decaracteres. Para satisfazer as duas formas, as strings podem ser escritas entre aspas duplas ouusando a notacao de lista de caracteres (entre aspas simples). Por exemplo, ”Constantino”temo mesmo significado que [’C’, ’o’, ’n’, ’s’, ’t’, ’a’, ’n’, ’t’, ’i’, ’n’, ’o’], em Haskell.

56

Page 65: LF Apostila

Todas as funcoes polimorficas do Prelude.hs podem ser verificadas no proximo Capıtulo.Estas funcoes podem ser usadas sobre strings, uma vez que elas sao definidas sobre listas dealgum tipo e uma string e tambem uma lista.

No entanto, deve ser feita uma observacao sobre a forma como uma string e exibida no moni-tor ou em outro dispositivo de saıda. As formas como Haskell trata as entradas e saıdas de seusdados sao descritas no Capıtulo 6, uma vez que seu entendimento requer um amadurecimentotecnico do leitor em relacao as linguagens funcionais, presumindo-se que ele ainda nao o tenhaneste ponto.

Toda aplicacao de funcao, em Haskell, produz um resultado. Podemos verificar isto atravesda tipificacao das funcoes. Ocorre, no entanto, que para mostrar um resultado, no monitorou em outro dispositivo de saıda, e necessario definir uma funcao para esta tarefa. E qualdeve ser o resultado desta operacao? Mostrar um valor no monitor nao implica em retornode qualquer valor como resultado. Algumas linguagens de programacao funcional, como ML,resolvem este problema de comunicacao com o mundo exterior atraves de atribuicoes destrutivas,o que descaracteriza a linguagem como funcional pura, transformando-a em impura.

No entanto, Haskell foi projetada como uma linguagem funcional pura e, para resolver estetipo de comunicacao, adota uma semantica de acoes baseada em Monadas, uma teoria bastantecomplexa. Uma acao em Haskell e um tipo de funcao que retorna um valor do tipo IO (),para ser coerente com o projeto da linguagem. Mostraremos aqui algumas funcoes usadas nacomunicacao com o mundo exterior e formas de aplicacao para facilitar o entendimento peloleitor. Caso contrario, seria bastante tedioso usar funcoes sem poder verificar os resultados desuas aplicacoes.

A primeira destas funcoes pre-definidas em Haskell e putStr :: String − > IO () que eutilizada para mostrar strings no monitor. Assim,

putStr ‘‘\99a\116’’ = catputStr ‘‘Dunga\teh o bicho’’ = Dunga eh o bichoputStr ‘‘jeri"++ ‘‘qua" ++ ‘‘quara" = jeriquaquara

Neste caso, as strings podem ser mostradas na tela do monitor. E se um valor nao for umstring? Neste caso, uma solucao e transformar o valor em uma string e agora pode-se usar afuncao putStr. Para esta missao, foi definida a funcao show :: t − > String que transformaum valor de qualquer tipo em uma string. Alem destas, a funcao read :: String − > t tomauma string como argumento de entrada e a transforma em um valor. Por exemplo,

show (5+7) = ‘‘12’’show (True && False) = ‘‘False’’

read ‘‘True’’ = Trueread ‘‘14’’ = 14

Exercıcios:

1. Defina uma funcao para converter letras minusculas em maiusculas e que retorne o propriocaractere se a entrada nao for um caractere minusculo.

2. Defina uma funcao charParaInt :: Char − > Int que converte um dıgito em seu valor(por exemplo, ’8’ em 8). O valor de um caractere nao dıgito deve ser 0 (zero).

3. Defina uma funcao imprimeDigito :: Char − > String que converte um dıgito em suarepresentacao em portugues. Por exemplo, ’5’ deve retornar ”cinco”.

57

Page 66: LF Apostila

4. Defina uma funcao romanoParaString :: Char − > String que converte um algarismoromano em sua representacao em Portugues. Por exemplo, romanoParaString ’V’ =”cinco”.

5. Defina uma funcao emTresLinhas :: String − > String − > String − > String quetoma tres strings e retorna um unico string mostrando os tres strings em linhas separadas.

6. Defina uma funcao replica :: String − > Int − > String que toma um String e umnumero natural n e retorna n copias da String, todas juntas. Se n for 0, o resultado deveser a String vazia (), se n for 1, retorna a propria String.

O tipo ponto flutuante (Float ou Double)

Os valores do tipo ponto flutuante (numeros reais) pertencem aos tipos Float ou Double,da mesma forma que um numero inteiro pertence aos tipos Int ou Integer. Isto significa queas unicas diferencas entre valores destes tipos se verificam na quantidade de bits usados pararepresenta-los. A Tabela 3.3 mostra as principais funcoes aritmeticas pre-definidas na linguagem.As funcoes aritmeticas recebem a denominacao especial de operadores.

Tabela 3.3: Operadores aritmeticos de ponto flutuante em Haskell.+, - * Float − > Float − > Float/ Float − > Float − > Floatˆ Float − > Int − > Float** Float − > Float − > Float==, /= <, >,<=, >= Float − > Float − > Boolabs Float − > Floatacos, asin, atan Float − > Floatceiling, floor, round Float − > Floatcos, sin, tan Float − > Floatexp Float − > FloatfromInt Int − > Floatlog Float − > FloatlogBase Float − > Float − > Floatnegate Float − > Floatread String − > Floatpi Floatshow * − > Stringsignum Float − > Intsqrt Float − > Float

Apesar de alguns pesquisadores, mais puristas, condenarem a sobrecarga de operadores, al-guns outros defendem que alguma forma de sobrecarga deve existir, para facilitar a codificacaode programas. Os projetistas de Haskell adimitiram a sobrecarga de operadores para ser utili-zada na implementacao de uma de suas caracterısticas importantes e que permite um grau deabstracao bem maior que normalmente se encontra em outras linguagens. Esta caracterıstica serefere as classes de tipos (type class), um tema a ser analisado no Capıtulo 5.

Exercıcio

Defina uma funcao mediadasVendas :: Int − > Float onde mediadasVendas n e amedia aritmetica entre os valores de vendas 0 ate vendas n.

58

Page 67: LF Apostila

Tabela 3.4: Quantidade de vendas por semana.Semana Vendas0 121 142 15Total 41Media 13.6667

3.4.2 Programando com numeros e strings

Duas metodologias de construcao de programas, bastante difundidas, sao a programacao top-down e a programacao bottom-up, de plena aceitacao pelos Engenheiros de Software. Haskellpermite que estas duas tecnicas sejam utilizadas em programas nela codificados. Elas seraomostradas a seguir.

Programacao top-down

Vamos nos referir novamente ao problema das vendas, descrito anteriormente. Suponhamos queas quantidades de vendas sejam as mostradas na Tabela 3.4.

Podemos construir uma funcao, imprimeTab, a partir da concatenacao de outras funcoesque constroem strings e serao desenvolvidas a seguir.

imprimeTab :: Int -> StringimprimeTab n = cabecalho ++ imprimeSemanas n ++ imprimeTotal

++ imprimeMedia n

Agora e necessario que estas funcoes componentes sejam definidas. Por exemplo, a funcaocabecalho e formada apenas por um string para representar os tıtulos dos ıtens da Tabela 3.4.

cabecalho :: Stringcabecalho = " Semana Vendas\n"

A funcao imprimeSemanas deve mostrar no monitor o numero de cada semana e a quan-tidade de vendas correspondente. Ela sera definida em funcao de uma outra funcao, imprime-Semana, que tambem tera que ser definida.

imprimeSemanas :: Int -> StringimprimeSemanas 0 = imprimeSemana 0imprimeSemanas n = imprimeSemanas (n-1) ++ imprimeSemana n

Faltam ser definidas as funcoes imprimeSemana, imprimeTotal e imprimeMedia quesao deixadas como exercıcio para o leitor.

Programacao bottom-up

A programacao top-down parte de um caso mais geral para casos mais especıficos, enquantoa modelagem bottom-up tem o sentido inverso desta orientacao. Ela inicia com as definicoesmais particulares, para depois compo-las em uma forma mais geral. Para exemplificarmos estasituacao, vamos utilizar a funcao rJustify :: Int − > String − > String, onde rJustify 10”Maria”= ” Maria”, a ser definida.

59

Page 68: LF Apostila

imprimeSemana :: Int -> StringimprimeSemana n = rJustify offset (show n) ++

rJustify offset (show (vendas n)) ++ "\n"where offset :: Int

offset = 10imprimeMedia :: Int -> StringimprimeMedia n = "\nMedia " ++ rJustify offset (show mediadeVendas n)

Exercıcios:

1. Defina uma funcao espacos :: Int − > String onde espacos n retorna um string de nespacos em branco.

2. Use a funcao espacos para definir uma funcao rJustify mencionada anteriormente.

3. De uma definicao de uma funcao tabeladeFatoriais :: Int − > Int − > String quemostre em forma de tabela os fatoriais dos inteiros de m ate n, inclusive de ambos.

4. Refaca a questao anterior adimitindo a possibilidade de entradas negativas e de que osegundo argumento seja menor que o primeiro.

3.4.3 Os tipos de dados estruturados de Haskell

Haskell tambem admite a possibilidade de que o usuario construa seus proprios tipos de da-dos, de acordo com as necessidades que ele tenha de simular problemas do mundo real. Ostipos estruturados sao construıdos a partir de outros tipos, primitivos ou estruturados. Esta euma caracterıstica muito importante desta linguagem, por facilitar a vida dos programadores,permitindo um grau muito maior de abstracao do problema a ser resolvido.

O tipo produto cartesiano

O produto cartesiano e representado em Haskell pelas tuplas, que podem ser duplas, triplas,quadruplas, etc. Na maioria das linguagens de programacao imperativas, este tipo de dados eimplementado atraves de registros ou estruturas. Em Haskell, o tipo (t1, t2, ..., tn) consiste den-uplas de valores (v1, v2, ..., vn) onde v1::t1, v2::t2, ..., vn::tn.

Por exemplo,

type Pessoa = (String, String, Int)maria :: Pessoamaria = ("Maria das Dores", "225-0000", 22)

intP :: (Int, Int)intP = (35, 45)

As funcoes sobre tuplas, apesar de poderem ser definidas de varias formas, sao comumentedefinidas por pattern matching.

somaPar :: (Int, Int) -> IntsomaPar (x, y) = x + y

Os padroes podem ter constantes e/ou padroes aninhados.

60

Page 69: LF Apostila

shift :: ((Int, Int), Int) -> (Int, (Int, Int))shift ((a,b),c) = (a, (b,c))

Podem ser definidas funcoes para mostrar casos particulares de uma tupla:

nome :: Pessoa -> Stringfone :: Pessoa -> Stringidade :: Pessoa -> Int

nome (n, p, a) = nfone (n, p, a) = pidade (n, p, a) = a

Assim, nome maria = ”Maria das Dores”

Deve-se ter algum cuidado com os tipos de dados que, em algumas situacoes, podem conduzira erros. Por exemplo, sao diferentes:

somaPar :: (Int, Int) -> Int e somaDois :: Int -> Int -> IntsomaPar (a, b) = a + b somaDois a b = a + b

Apesar dos resultados das aplicacoes das funcoes somaPar e somaDois serem os mesmos,elas sao distintas. A funcao somaPar requer apenas um argumento, neste caso uma tupla,enquanto a funcao somaDois requer dois argumentos do tipo inteiro.

3.4.4 Escopo

O escopo de uma definicao e a parte de um programa na qual ela e visıvel e portanto pode serusada. Em Haskell, o escopo das definicoes e todo o script, ou seja, todo o arquivo no qual adefinicao foi feita. Por exemplo, vejamos a definicao de ehImpar n, a seguir, que menciona afuncao ehPar, apesar desta ser definida depois. Isto so e possıvel porque elas compartilham omesmo escopo.

ehImpar, ehPar :: Int -> BoolehImpar 0 = FalseehImpar n = ehPar (n-1)

ehPar 0 = TrueehPar n = ehImpar (n-1)

Definicoes locais

Haskell permite definicoes locais atraves da palavra reservada where. Por exemplo,

somaQuadrados :: Int -> Int -> IntsomaQuadrados n m = quadN + quadM

wherequadN = n * nquadM = m * m

As definicoes locais podem incluir outras definicoes de funcoes, alem de poder usar definicoeslocais a uma expressao, usando a palavra reservada let.

61

Page 70: LF Apostila

let x = 3 + 2; y = 5 - 1 in xˆ2 + 2*x*y - y

As definicoes locais sao visıveis apenas na equacao onde elas foram declaradas. As variaveisque aparecem do lado esquerdo da igualdade tambem podem ser usadas em definicoes locais, dolado esquerdo. Por exemplo,

maximoQuadrado x y|quadx > quady = quadx|otherwise = quady

wherequadx = quad xquady = quad yquad :: Int -> Intquad z = z * z

As definicoes locais podem ser usadas antes delas serem definidas e tambem podem ser usadasem resultados, em guardas ou em outras definicoes locais. Como exemplo,

maximasOcorrencias :: Int -> Int -> Int -> (Int, Int)maximasOcorrencias n m p = (max, quantosIguais)

wheremax = maximoDeTres n m pquantosIguais = quantosIguaisValor max n m pmaximoDeTres :: Int -> Int -> Int -> IntmaximoDeTres a b c = maximo (maximo (a, b), c)quantosIguaisValor :: Int -> Int -> Int -> Int -> Int

onde a funcao quantosIguaisValor pode ser definida de uma das formas mostradas na Tabelaa seguir.

quantosIguaisValor valor n m p quantosIguaisValor valor n m p= ehN + ehM + ehP = ehvalor n + ehvalor m + ehvalor p

where whereehN = if n == valor then 1 else 0 ehvalor :: Int − > IntehM = if m == valor then 1 else 0 ehvalor x = if x == valor then 1 else 0ehP = if p == valor then 1 else 0

A construcao if c then e1 else e2 e valida em Haskell e avalia a expressao booleanac, tendo como resultado e1 (se c for True) ou e2 (se c for False). Na definicao da funcaoquantosIguaisValor o construtor if foi utilizado varias vezes.

3.4.5 Calculos:

A avaliacao usada por Haskell e chamada de avaliacao preguicosa, onde cada expressao e avaliadaapenas uma vez e se necessario. Um calculo so e realizado se for realmente necessario e seu valore colocado em uma celula da heap. Se ele for novamente solicitado, ja esta calculado e prontopara ser utilizado. Por este motivo, Haskell, a exemplo de qualquer linguagem funcional e todasas modernas linguagens de programacao, faz o gerenciamento dinamico de memoria de formaautomatica, ou seja, pelo sistema. Este processo de gerenciamento da memoria dinamica deforma automatica e chamado de Garbage Collection ou Coleta de Lixo. Para mais detalhessobre Coleta de Lixo, o leitor deve consultar as referencias [8, 9, 10, 11, 12].

62

Page 71: LF Apostila

Vamos mostrar, detalhadamente, a sequencia de avaliacao da aplicacao de duas funcoes jadefinidas anteriormente, somaquadrados e maximasOcorrencias.

somaQuadrados 4 3 = quadN + quadMwherequadN = 4 * 4 = 16quadM = 3 * 3 = 9

= 16 + 9= 25

maximasOcorrencias 2 1 2 = (max, quantosIguais)wheremax = maximoDeTres 2 1 2

= maximo (maximo 2 1) 2?? 2>=1 = True

= maximo 2 2?? 2>=2 = True

= 2= (2, quantosIguais)

wherequantosIguais

= quantosIguaisValor 2 2 1 2= ehValor 2 + ehvalor 1 + ehvalor 2

whereehvalor 2 = if 2 == 2 then 1 else 0

= if True then 1 else 0= 1

= 1 + ehvalor 1 + ehvalor 2whereehvalor 1 = if 1 == 2 then 1 else 0

= if False then 1 else 0= 0

= 1 + 0 + ehvalor 2whereehvalor 2 = if 2 == 2 then 1 else 0

= if True then 1 else 0= 1

= 1 + 0 + 1= 2

= (2, 2)

Esta sequencia de calculos deve ser acompanhada pelo leitor para entender a forma comoo compilador (ou interpretador) Haskell executa seus calculos. Este entendimento tem im-portancia fundamental na construcao de funcoes, notadamente de funcoes que tratam com listaspotencialmente infinitas, a serem vistas mais adiante.

Exercıcios:

1. Calcule os valores das expressoes: maximasOcorrencias 1 2 1 e quantosIguaisValor4 2 1 3.

2. Defina uma funcao cJustify :: Int − > String − > String onde cJustify n st retornauma string de tamanho n, adicionando espacos antes e depois de st para centraliza-la.

63

Page 72: LF Apostila

3. Defina uma funcao stars :: Int − > String de forma que stars 3 retorna “***”. Comodeve ser tratada uma entrada negativa?

Exemplo. Vejamos agora construir um exemplo bastante conhecido que e o de encontrar asraızes reais de uma equacao do segundo grau, baseado em [35]. Neste caso teremos como entradaa equacao a ∗ x2 + b ∗ x + c = 0, sendo a = 1.0, b = 5.0 e c = 6.0.

Para esta solucao, a saıda sera a string

A equacao 1.0 * x ˆ2 + 5.0 * x + 6.0 = 0.0

tem duas raızes reais e distintas: -2.0 e -3.0.

Para isto vamos construir duas funcoes: umaRaiz para o caso da funcao ter duas raızesreais e iguais e duasRaizes para o caso dela ter duas raızes reais e distintas.

umaRaiz :: Float -> Float -> Float -> FloatumaRaiz a b c = -b / (2.0 * a)

duasRaizes :: Float -> Float -> Float -> (Float, Float)duasRaizes a b c = (d + e, d - e)

whered = -b/(2.0*a)e = sqrt (b^2 - 4.0*a*c)/(2.0*a)

saida :: Float -> Float -> Float -> Stringsaida a b c = cabecalho a b c ++ raizes a b c

cabecalho :: Float -> Float -> Float -> Stringcabecalho a b c = "A equacao \n\n\t"++ show a ++ "*x^2 + " ++

show b ++ "*x + " ++ show c ++ " = 0.0" ++ "\n\ntem "++ raizes a b c

raizes :: Float -> Float -> Float -> Stringraizes a b c

| b^2 > 4.0 * a * c = "duas raizes reais e distintas: "++ show f ++ ‘‘ e ‘‘ ++ show s

|b^2 == 4.0 * a * c = "duas raizes reais e iguais: "++ show (umaRaiz a b c)

|otherwise = "nenhuma raiz real "where (f, s) = duasRaizes a b c

Na equacao do segundo grau, se a entrada para o coeficiente a for zero, nao sera possıvela divisao de qualquer numero por ele. Neste caso, o programa deve abortar a execucao e umaexcecao deve ser feita para descrever o motivo. A funcao umaRaiz deve ser re-definida daseguinte forma:

umaRaiz a b c|(a /= 0.0) = -b/ (2.0 * a)|otherwise = error "umaRaiz chamada com a == 0"

A redefinicao da funcao duasRaizes e deixada para o leitor, como exercıcio.

64

Page 73: LF Apostila

3.4.6 Projeto de programas

A Engenharia de Software admite algumas metodologias para a construcao de programas, deforma a obter melhores resultados, tanto em relacao as solucoes quanto em relacao ao tempo dedesenvolvimento. A sequencia de passos mostrada a seguir, devida a Simon Thompson [35], econsiderada um bom roteiro na codificacao de programas para a solucao de problemas usandoHaskell:

• Verificar problemas similares e mais simples.

• Decidir os tipos para representacao.

• Dividir para conquistar (abordagem top-down).

• Uma solucao para um caso menor pode ser utilizada para um caso maior.

• O uso de clausulas where.

• Se uma expressao aparecer mais de uma vez, e forte candidata a ser declarada como umafuncao.

• Usar uma abordagem bottom-up.

• O layout do script e importante.

Exercıcios

Para os exercıcios a seguir, considere os pontos do plano como sendo do tipo Ponto =(Float, Float). As linhas do plano sao definidas por seus pontos inicial e final e tem o tipoLinha = (Ponto, Ponto).

1. Defina funcoes que retornem a ordenada e a abcissa de um ponto.

2. Defina uma funcao que verifica se uma linha e vertical ou nao.

3. Se uma linha e determinada pelos pontos (x1, y1) e (x2, y2), sua equacao e definida por(y - y1)/(x - x1) = (y2 - y1)/(x2 - x1). Defina uma funcao do tipo valorY :: Float − >Linha − > Float que retorna a ordenada y do ponto (x,y), sendo dados x e uma linha.

3.4.7 Provas de programas

Uma prova e uma argumentacao logica ou matematica para verificar se alguma premissa e ounao valida, em quaisquer circunstancias.

Este tema tem importancia fundamental na construcao de programas, uma vez que deve-seter a garantia de que o programa esteja correto e que ele realiza apenas a acao para a qual foicriado. A prova de programas aumenta sua importancia a cada dia, uma vez que os problemasestao se tornando cada vez mais complexos e deve-se ter a certeza de que o programa para resolve-lo esteja correto. Os problemas se tornam cada vez mais desafiadores, por exemplo, problemasnucleares ou outros que envolvam vidas humanas podendo colocar em jogo suas sobrevivencias.Para estes casos, ha de existir uma prova de que ele produz a solucao correta.

No entanto, existe um dilema da parte de usuarios que, as vezes, tem dificuldades de realizarprovas de programas, achando ser uma tecnica de fundamentacao matematica e trabalhosa. Aprova de programas em uma linguagem imperativa e realmente tediosa, no entanto, como seravisto, ela e bem menos difıcil em uma linguagem funcional como Haskell.

65

Page 74: LF Apostila

Existem em Haskell tres maneiras de realizar provas: a prova direta, a prova por casos e aprova por inducao matematica. Vamos analisa-las atraves de exemplos, lembrando ao leitor queo domınio dessas tecnicas so e conseguido atraves da pratica.

Provas diretas

A prova direta e feita aplicando as definicoes das funcoes. Por exemplo, sejam as funcoes troca,cicla e recicla, definidas da seguinte forma:

troca :: (Int, Int) -> (Int, Int)troca (a, b) = (b, a) --def 1

cicla, recicla :: (Int, Int, Int) -> (Int, Int, Int)cicla (a, b, c) = (b, c, a) --def 2recicla (a, b, c) = (c, a, b) --def 3

A partir destas definicoes, podemos provar assertivas nas quais estejam envolvidas. Porexemplo, podemos provar que troca (troca (a, b)) = (a, b), ou ainda que cicla (recicla (a,b, c)) = recicla (cicla (a, b, c)). Vejamos como isto pode ser feito:

troca (troca (a, b)) = troca (b, a) --por def 1= (a, b) --por def 1

cicla (recicla (a, b, c)) = cicla (c, a, b) = (a, b, c) --por def 3 e 2recicla(cicla (a, b, c)) = recicla (b, c, a) = (a, b, c) --por def 2 e 3

Portanto, sao iguais os resultados e a prova esta completa. Facil, nao?

Provas por casos

Seja a definicao da funcao maximo, ja feita anteriormente no inıcio do Capıtulo:

maximo :: Int -> Int -> Intmaximo n m|n >= m = n --def 1|otherwise = m --def 2

Seja a assertiva: “Para quaisquer numeros inteiros n e m, maximo n m ≥ n”.

Para quaisquer numeros m e n definidos, tem-se: m > n ou n ≥ m. Entao,

• Caso 1: se n ≥ m: maximo n m = n (def 1) e n ≥ n. Portanto, maximo n m ≥ n.

• Caso 2: se m > n: maximo n m = m (def 2) e m > n. Portanto, maximo n m > n. Logo,maximo n m ≥ n.

Como outro exemplo, vamos definir a funcao maxVendas da seguinte maneira:

maxVendas 0 = vendas 0maxVendas r = maximo (maxVendas (r-1)) (vendas r)

66

Page 75: LF Apostila

Agora podemos provar que maxVendas r ≥ maxVendas (r - 1) usando a propriedademostrada para a funcao maximo.

Exercıcios

1. Prove que cicla (cicla (cicla (a, b, c))) = (a, b, c) para todo a, b e c.

2. Sendo somaTres (a, b, c) = a + b + c, para a, b e c inteiros, de uma prova de quesomaTres (cicla (a, b, c)) = somaTres (a, b, c).

3. Dada a definicaotrocaSe :: (Int, Int) − > (Int, Int)trocaSe (a, b)

| a <= b = (a, b)| otherwise = (b, a)

Prove que para todo a e b definidos, trocaSe(trocaSe(a, b)) = trocaSe(a, b).

Inducao matematica

Da Matematica, sabemos que o esquema de prova por inducao dentro do conjunto dos numerosnaturais e uma forma muito comum. Um sistema similar pode ser utilizado para provar progra-mas codificados em Haskell.

Para provar que uma propriedade P(n) e valida para todo natural n, deve-se:

• Caso base: Provar P(n), para n = 0.

• Passo indutivo: Para n > 0, provar P(n), assumindo que P(n-1) e valida.

Vejamos, por exemplo, a funcao fatorial:

fatorial 0 = 1 -- (fat 1)fatorial n = n * fatorial (n - 1) -- (fat 2)

Podemos agora provar a seguinte propriedade dos naturais:

P(n): fatorial n > 0, para todo natural n.

O esquema de prova e feito da seguinte forma:

• Caso base (P(0)): fatorial 0 = 1 (por fat 1) e 1 > 0. Logo fatorial 0 > 0, significando quea propriedade e valida para o caso base.

• Passo indutivo (P(n)): fatorial n = n * fatorial (n-1), (por fat 2) admitindo-se que n>0.A hipotese de inducao informa que fatorial (n-1) > 0, ou seja, a propriedade P e validapara n-1. Assim o fatorial de n e o produto de dois fatores sendo ambos maiores que zero,ou seja, temos >0 * >0. O produto de dois numeros positivos e tambem positivo. Logo,maior que 0.

• Conclusao: como a propriedade P e valida para o caso base e para o passo indutivo, entaoela e valida para todo n natural.

Esta ultima parte, a conclusao da prova, e um componente importante da prova. E comumver um esquema de prova, normalmente feito por iniciantes, onde os dois passos sao verificados,mas nao existe a conclusao. Neste caso, a prova esta incompleta.

67

Page 76: LF Apostila

Provas por Inducao

Enquanto a inducao formula provas para P(0), P(1), ..., a definicao recursiva de fatorial, vistaanteriormente, constroi resultados para fatorial 0, fatorial 1, .... “A forma como P(n-1) eassumida para se provar P(n) e semelhante a forma usada por fatorial (n-1) para encontrar ovalor de fatorial (n)”.

Este esqema de prova normalmente e aplicado a funcoes definidas por recursao primitivarepresentando tao somente um processo de traducao semelhante ao esquema de prova por inducaomatematica.

Simon Thompson escreveu um guia de passos a serem seguidos nos esquemas de provas porinducao em Haskell [35]. A sequencia de passos de provas proposta por ele e instrutiva e servede roteiro, principalmente para quem esta dando os primeiros passos em direcao a este estudo.Com o decorrer do tempo, este esquema passa a ser um processo automatico.

Estagio 0: escrever o objeto da prova em portugues,Estagio 1: escrever o objeto da prova em linguagem formal,Estagio 2: escrever os sub-objetos da prova por inducao:

P(0):P(n), para todo n>0, assumindo P(n-1)

Estagio 3: Provar P(0)Estagio 4: Provar P(n), para n>0, lembrando que deve e pode usar P(n-1)

Exemplo: Sejam as definicoes das funcoes a seguir:

power2 :: Int -> Intpower2 0 = 1 (1)power2 r = 2 * power2 (r - 1) (2)

sumPowers :: Int -> IntsumPowers 0 = 1 (3)sumPowers r = sumPowers (r-1) + power2 r (4)

Prove que sumPowers n + 1 = power2 (n + 1).

Estagio 0: provar que a soma das potencias de 2 de 0 a n, adicionada a 1e igual a (n + 1)-esima potencia de 2.

Estagio 1: provar P(n): sumPowers n + 1 = power2 (n+1)Estagio 2: sumPowers 0 + 1 = = power2 (0 + 1), para n = 0?

sumPowers n + 1 = = power2 (n + 1), para n > 0?,assumindo que sumPowers (n - 1) + 1 = power2 n

Estagio 3: sumPowers 0 + 1 = 1 + 1 = 2 –por (3)power2 (0 + 1) = 2 * power2 0 = 2 * 1 = 2 –por (2)

logo, a prova e valida para o caso base.

sumPowers n + 1 = sumPowers (n-1) + power2 n + 1 –por (4)= sumPowers(n-1) + 1 + power2 n –pela comutatividade de += power2 n + power2 n –pela hip. de inducao= 2 * power2 n= power2 (n+1) –por (2)

Exercıcios

1. Prove que, para todo numero natural n, fatorial (n + 1) ≥ power2 n.

2. Prove que, para todo numero natural n, fib (n+1) ≥ power2 (n div 2).

68

Page 77: LF Apostila

3. De uma prova de que, para todo numero natural n, vendas n ≥ 0.

3.5 Resumo

Neste Capıtulo, foi dado inıcio ao estudo de programacao em Haskell. Foram vistos os tipos dedados primitivos e as tuplas. Alguns exercıcios foram resolvidos para dar uma nocao ao usuarioda potencialidade da linguagem e outros foram deixados para o leitor.

O livro de Simon Thompson [35] foi a fonte mais utilizada para o estudo mostrado noCapıtulo, por ser um livro que apresenta um conteudo teorico bem estruturado e fundamentado,alem de muitos exercıcios resolvidos e muitos problemas propostos.

O livro de Richard Bird [4] e outra fonte importante de exercıcios resolvidos e propostos,apesar de sua sequencia de abordagens seguir uma ordem distinta, exigindo um conhecimentoanterior sobre programacao funcional, o que o torna mais defıcil de ser seguido por iniciantesneste tema.

Outra referencia importante e o livro de Paul Hudak [14] que apresenta a programacaofuncional em Haskell com exemplos aplicados a Multimıdia, envolvendo a construcao de umeditor grafico e de um sistema utilizado em musica, entre outros. Para quem imagina queHaskell so e aplicado a problemas da Matematica, esta referencia poe por terra este argumento.

69

Page 78: LF Apostila

70

Page 79: LF Apostila

Capıtulo 4

O tipo Lista

”Commputers specially designed for applicative languagesimplement recursive functions very efficiently.

Also, architectural features can be included in conventionalcomputers that significantly increase

the speed of recursive-function invocations.”(Bruce J. MacLennan in [24])

Lista e o tipo de dado mais importante nas linguagens funcionais. Todas as linguagensfuncionais a implementam como um tipo primitivo, juntamente com uma gama imensa de funcoespara a sua manipulacao. A notacao utilizada para as listas e colocar seus elementos entrecolchetes. Por exemplo, [1,2,3,4,1,3] e uma lista de inteiros, [True,False] e uma lista debooleanos, [’a’, ’a’, ’b’] e uma lista de caracteres e [”Marta”, ”Marlene”] e uma lista destrings. Ha, no entanto, que se diferenciar as listas homogeneas, que sao as listas onde todosos valores sao do mesmo tipo, das listas heterogeneas, onde os componentes podem ter mais deum tipo. Haskell so admite listas homogeneas. Por exemplo, [False, 2,”Maria”] nao e umalista em Haskell, por ser heterogenea. Em compensacao, podemos ter a lista [totalVendas,totalVendas], que tem o tipo [Int − > Int] como tambem a lista [[12,1], [3,4], [4,4,4,4,4],[ ]] que tem o tipo [[Int]], uma lista de listas de inteiros, alem de outras possibilidades.

Existem duas formas como as listas podem se apresentar:

• a lista vazia, simbolizada por [ ], que pode ser de qualquer tipo. Por exemplo, ela podeser de inteiros, de booleanos, etc. Dito de outra forma, [ ] e do tipo [Int] ou [Bool] ou[Int − > Int], significando que a lista vazia esta na intersecao de todas as listas, sendo ounico elemento deste conjunto.

• a lista nao vazia, simbolizada por (a : x), onde a representa um elemento da lista,portanto tem um tipo, e x representa uma lista composta de elementos do mesmo tipo dea. O elemento a e chamado de cabeca e x e a cauda da lista.

Algumas caracterısticas importantes das listas em Haskell, sao:

• A ordem em uma lista e mportante, ou seja, [1,3] /= [3,1] e [False] /= [False, False].

• A lista [n .. m] e igual a lista [n, n+1, ..., m]. Por exemplo, [1 .. 5] = [1, 2, 3, 4, 5].A lista [3.1 .. 7.0] = [3.1, 4.1, 5.1, 6.1].

• A lista [n,p .. m] e igual a lista de n ate m em passos de p-n. Por exemplo, [7,6 ..3] =[7, 6, 5, 4, 3] e [0.0, 0.3 ..1.0] = [0.0, 0.3, 0.6, 0.9].

71

Page 80: LF Apostila

• A lista [n ..m], para n>m, e vazia. Por exemplo, [7 .. 3] = [ ].

• A lista vazia nao tem cabeca e nem cauda. Se tivesse qualquer destes dois componentes,nao seria vazia.

• A lista nao vazia tem cabeca e cauda, onde a cauda e tambem uma lista, que pode servazia, ou nao.

4.1 Funcoes sobre listas

As funcoes para a manipulacao de listas sao declaradas da mesma forma como sao declaradaspara processar outros tipos de dados, usando casamento de padroes. Neste caso, os padroes saoapenas dois: a lista vazia e a lista nao vazia. Por exemplo,

somaLista :: [Int] -> IntsomaLista [ ] = 0somaLista (a:x) = a + somaLista x

A funcao somaLista toma como argumento uma lista de inteiros e retorna, como resultado,um valor que e a soma de todos os elementos da lista argumento. Se a lista for vazia ([ ]), asoma sera 0. Se a lista nao for vazia (a : x), o resultado sera a soma de sua cabeca (a) como resultado da aplicacao da mesma funcao somaLista a cauda da lista (x). Esta definicaoe recursiva, uma caracterıstica muito utilizada, por ser a forma usada para fazer iteracao emprogramas funcionais. Devemos observar que a ordem em que os padroes sao colocados temimportancia fundamental. No caso em voga, primeiramente foi feita a definicao para a listavazia e depois para a lista nao vazia. Dependendo da necessidade do programador, esta ordempode ser invertida.

Vejamos a sequencia de calculos da aplicacao da funcao somaLista a lista [2, 3, 5, 7].

somaLista [2,3,5,7] =2 + somaLista [3,5,7]=2 + (3 + somaLista [5,7])=2 + (3 + (5 + somaLista [7]))=2 + (3 + (5 + (7 + somaLista [])))=2 + (3 + (5 + (7 + 0)))=2 + (3 + (5 + 7))=2 + (3 + 12)=2 + 15=17

4.1.1 O construtor de listas : (cons)

O construtor de listas, chamado de cons e sinalizado por : (dois pontos), tem importancia fun-damental na construcao de listas. Ele e um operador que toma como argumentos um elemento,de um tipo, e uma lista de elementos deste mesmo tipo e insere este elemento como a cabeca danova lista. Por exemplo,

10 : [ ] = [10]2 : 1 : 3 : [ ] = 2 : 1 : [3] = 2 : [1,3] = [2,1,3]

significando que cons (:) associa seus componentes pela direita, ou seja:

72

Page 81: LF Apostila

a : b : c = a : (b : c) /= (a : b) : c

Mas qual o tipo de cons? Observando que 4 : [3] = [4, 3], entao cons tem o tipo:

(:) :: Int -> [Int] -> [Int]

No entanto, verificamos tambem que True : [False] = [True, False]. Agora cons tem otipo:

(:) :: Bool -> [Bool] -> [Bool]

Isto mostra que o operador cons e polimorfico. Desta forma, seu tipo e:

(:) :: t -> [t] -> [t]

onde [t] e uma lista de valores, de qualquer tipo, desde que seja homogenea.

4.1.2 Construindo funcoes sobre listas

Em Haskell, ja existe um grande numero de funcoes pre-definidas para a manipulacao de listas.Estas funcoes fazem parte do arquivo Prelude.hs, carregado no momento em que o sistema echamado e permanece ativo ate o final da execucao. Um resumo destas funcoes, com seus tipose exemplos de utilizacao, pode ser visto na Tabela 4.1.

Tabela 4.1: Algumas funcoes polimorficas do Prelude.hs.Funcao Tipo Exemplo: a− > [a]− > [a] 3:[2,5]=[3,2,5]++ [a]− > [a]− > [a] [3,2]++[4,5]=[3,2,4,5]!! [a]− > Int− > a [3,2,1]!!1=3concat [[a]]− > [a] [[2],[3,5]]=[2,3,5]length [a]− > Int length [3,2,1]=3head [a]− > a head [3,2,5]=3last [a]− > a last [3,2,1]=1tail [a]− > [a] tail [3,2,1]=[2,1]init [a]− > [a] init [3,2,1]=[3,2]replicate Int− > a− > [a] replicate 3 ’a’=[’a’,’a’,’a’]take Int− > [a]− > [a] take 2 [3,2,1]=[3,2]drop Int− > [a]− > [a] drop 2 [3,2,1]=[1]splitAt Int− > [a]− > ([a], [a]) splitAt 2 [3,2,1]=([3,2],[1])reverse [a]− > [a] reverse [3,2,1]=[1,2,3]zip [a]− > [b]− > [(a, b)] zip[3,2,1][5,6]=[(3,5),(2,6)]unzip [(a, b)]− > ([a], [b]) unzip [(3,5),(2,6)]=([3,2],[5,6])and [Bool]− > Bool and [True,False]=Falseor [Bool]− > Bool or [True,False]=Truesum [Int]− > Int sum [2,5,7]=14

[Float]− > Float sum [3.0,4.0,1.0]=8.0product [Int]− > Int product [1,2,3]=6

[Float]− > Float product [1.0,2.0,3.0]=6.0

73

Page 82: LF Apostila

Alem das funcoes pre-definidas, o usuario tambem pode construir funcoes para manipularlistas. Aqui a criatividade e o limite. Vamos mostrar isto atraves de um exemplo simples edepois atraves de um exemplo mais complexo.

Vamos construir uma funcao que verifica se um determinado elemento pertence, ou nao, auma lista. Para isto vamos construir a funcao fazParte:

fazParte :: [Int] -> Int -> BoolfazParte [ ] b = FalsefazParte (a:x) b = (a == b) || fazParte x b

Esta mesma funcao tambem pode ser codificada de outra forma, usando guardas:

fazParte [ ] b = FalsefazParte (a:x) b|a == b = True|otherwise = fazParte x b

Exercıcios

1. Dada a definicao da funcao dobra

dobra :: [Int] -> [Int]dobra [ ] = [ ]dobra (a:x) = (2 * a) : dobra x

Calcule dobra [3,4,5] passo a passo.

2. Escreva [False, False, True] e [2] usando : e [ ].

3. Calcule somaLista [30, 2, 1, 0], dobra [0] e “cafe”++”com”++ “leite”.

4. Defina uma funcao produtoLista :: [Int] − > Int que retorna o produto de uma listade inteiros.

5. Defina uma funcao and :: [Bool] − > Bool que retorna a conjuncao da lista. Porexemplo, and [e1, e2, . . . , en] = e1&&e2&& . . . &&en (a conjuncao da lista vazia e True).

6. Defina uma funcao concatena :: [[Int]] − > [Int] que concatena uma lista de listasde inteiros transformando-a em uma lista de inteiros. Por exemplo, concat [[3,4], [2],[4,10]] = [3,4,2,4,10].

Vamos agora mostrar um exemplo mais complexo, envolvendo a ordenacao de uma lista deinteiros.

Uma forma de ordenar uma lista nao vazia e inserir a cabeca da lista no local correto, quepode ser na cabeca da lista ou pode ser na cauda ja ordenada. Por exemplo, para ordenar alista de inteiros [3,4,1], devemos inserir 3 na cauda da lista, ja ordenada, ou seja em [1,4]. Paraque esta cauda ja esteje ordenada e necessario apenas chamar a mesma funcao de ordenacao,recursivamente, para ela. Vamos definir uma funcao de ordenacao, ordena, que utiliza umafuncao auxiliar insere, cuja tarefa e inserir cada elemento da lista no lugar correto.

ordena :: [Int] -> [Int]ordena [ ] = [ ]ordena (a:x) = insere a (ordena x)

74

Page 83: LF Apostila

A lista vazia e considerada ordenada por vacuidade. Por isto a primeira definicao. Para ocaso da lista nao vazia, devemos inserir a cabeca (a) na cauda ja ordenada (ordena x). Faltaapenas definir a funcao insere, que e auto-explicativa.

insere :: Int -> [Int] -> [Int]insere a [ ] = [a]insere a (b:y)|a <= b = a : (b : y) -- a serah a cabeca da lista|otherwise = b : insere a y -- procura colocar a no local correto

Este metodo de ordenacao e conhecido como insercao direta e o leitor deve observar asimplicidade como ele e implementado em Haskell. Sugerimos comparar esta implementacaocom outra, em qualquer linguagem convencional. Alem disso, tambem se deve considerar apossibilidade de aplicacao desta mesma definicao a listas de varios tipos de dados, significandoque a definicao pode ser polimorfica. Isto significa que, na definicao da funcao insere, a unicaoperacao exigida sobre os valores dos elementos da lista a ser ordenada e que eles possam sercomparados atraves da operacao ≤. Uma lista de valores, de qualquer tipo de dados, onde estaoperacao seja possıvel entre estes valores, pode ser ordenada usando esta definicao.

Exercıcios

1. Mostre todos os passos realizados na chamada ordena [2, 8, 1].

2. Defina uma funcao numOcorre :: [t] − > t − > Int, onde numOcorre l s retorna onumero de vezes que o ıtem s aparece na lista l.

3. De uma definicao dia funcao fazParte usando a funcao numOcorre, do ıtem anterior.

4. Defina uma funcao unico :: [Int] − > [Int] que retorna a lista de numeros que ocorremexatamente uma vez em uma lista. Por exemplo, unico [2,4,2,1,4] = [1].

4.2 Pattern matching revisado

O casamento de padroes ja foi analisado anteriormente, mas sem qualquer profundidade e for-malismo. Agora ele sera visto com uma nova roupagem, apesar de usar conceitos ja conhecidos.

Os padroes em Haskell sao dados por:

• valores literais como -2, ’C’ e True.

• variaveis como x, num e maria.

• o caractere (sublinhado) casa com qualquer argumento.

• um padrao de tuplas (p1, p2, ..., pk). Um argumento para casar com este padrao tem de serda forma (v1, v2, ..., vk), onde cada vi deve ser do tipo pi.

• um construtor aplicado a outros padroes. Por exemplo, o construtor de listas (p1 : p2),onde p1 e p2 sao padroes

Nas linguagens funcionais, a forma de verificar se um padrao casa, ou nao, com um dado erealizada da seguinte maneira:

• primeiramente, analisa-se se o argumento esta na forma correta e

75

Page 84: LF Apostila

• depois associam-se valores as variaveis dos padroes.

Exemplo. A lista [2,3,4] casa com o padrao (a : x) porque:

1. ela tem cabeca e tem cauda, portanto e uma lista correta. Portanto,

2. 2 e associado com a cabeca a e [3,4] e associada com a cauda x.

Pode-se perguntar: em quais situacoes um argumento a casa com um padrao p? Esta questaoe respondida atraves da seguinte lista de clausulas:

• se p for uma constante, a casa com p se a == p.

• se p for uma variavel, x casa com p e x sera associado com p.

• se p for uma tupla de padroes (p1, p2, ..., pk), a casa com p se a for uma tupla (a1, a2, ..., ak)e se cada ai casar com cada pi.

• se p for uma lista de padroes (p1 : p2), a casa com p se a for uma lista nao vazia. Nestecaso, a cabeca de a e associada com p1 e a cauda de a e associada com p2.

• se p for um sublinhado ( ), a casa com p, mas nenhuma associacao e feita. O sublinhadoage como se fosse um teste.

Exemplo. Seja a funcao zip definida da seguinte forma:

zip (a:x) (b:y) = (a, b) : zip x yzip _ _ = [ ]

Se os argumentos de zip forem duas listas, ambas nao vazias, forma-se a tupla com as cabecasdas duas listas, que sera incorporada a lista de tuplas resultante e o processo continua com aaplicacao recursiva de zip as caudas das listas argumentos. Este processo continua ate que opadrao de duas listas nao vazias falhar. No momento em que uma das listas, ou ambas, forvazia, o resultado sera a lista vazia e a execucao da funcao termina. Vamos verificar o resultadode algumas aplicacoes.

zip [2,3,4] [4,5,78] = [(2,4), (3,5), (4,78)].zip [2,3] [1,2,3] = [(2,1),(3,2)]

Exercıcios

1. Defina uma funcao somaTriplas que soma os elementos de uma lista de triplas de numeros,(c, d, e).

2. Defina uma funcao somaPar para somar os elementos de uma lista de pares de numeros,((c,d), (e,f)).

3. Calcule somaPar [(2,3), (96, -7)], passo a passo.

4. Defina uma funcao unzip :: [(Int, Int)] − > ([Int], [Int]) que transforma uma lista depares em um par de listas. Sugestao: defina antes as funcoes unZipLeft, unZipRight ::[(Int, Int)] − > [Int], onde unZipLeft [(2,4), (3,5), (4,78)] = [2,3,4] e unZipRight[(2,4), (3,5), (4,78)] = [4,5,78].

76

Page 85: LF Apostila

Exemplo: Agora vamos analisar um exemplo pratico da aplicacao de listas em Haskell, baseadoem Simon Thompson [35]. Seja um banco de dados definido para contabilizar as retiradas delivros de uma Biblioteca, por varias pessoas. Para simular esta situacao, vamos construir umalista de tuplas compostas pelo nome da pessoa que tomou emprestado um livro e do tıtulo dolivro. Para isto, teremos:

type Pessoa = Stringtype Livro = Stringtype BancodeDados = [(Pessoa, Livro)]

Vamos construir uma lista fictıcia para servir apenas de teste, ou seja, vamos supor que, emum determinado momento, a lista esteja composta das seguintes tuplas:

teste = [("Paulo", "A Mente Nova do Rei"), ("Ana", "O Segredo de Luiza"),("Paulo", "O Pequeno Principe"), ("Mauro", "O Capital"),("Francisco", "O Auto da Compadecida")]

Vamos definir funcoes para realizar as seguintes tarefas:

1. Operaccoes de consulta:

• Uma funcao que informa os livros que uma determinada pessoa tomou emprestado.• Uma funcao que informa todas as pessoas que tomaram emprestado um determinado

livro.• Uma funcao que informa se um determinado livro esta ou nao emprestado.• Uma funcao que informa a quantidade de livros que uma determinada pessoa tomou

emprestado.

2. Operacoes de atualizacao:

• Uma funcao que atualiza o banco, quando um livro e emprestado a alguem.• Uma funcao que atualiza o banco quando um livro e devolvido.

Inicialmente, vamos construir a funcao livrosEmprestados que pode ser utilizada paraservir de roteiro para a definicao das outras funcoes de consulta, deixadas, como exercıcio, parao leitor.

livrosEmprestados :: BancodeDados -> Pessoa -> [Livro]livrosEmprestados [ ] _ = [ ]livrosEmprestados ((inquilino, titulo) : resto) fulano| inquilino == fulano = titulo : livrosEmprestados resto fulano| otherwise = livrosEmprestados resto fulano

Vamos agora mostrar as definicoes das funcoes de atualizacao:

tomaEmprestado :: BancodeDados -> Pessoa -> Livro -> BancodeDadostomaEmprestado dBase pessoa titulo = (pessoa, titulo) : dBase

devolveLivro :: BancodeDados -> Pessoa -> Livro -> BancodeDadosdevolveLivro ((p, t): r) f l| p == f && t == l = r| otherwise = (p,t) : devolveLivro r f l

devolveLivro [ ] ful tit= error ("returnLoan failed on "++ ful ++ " " ++ tit)

77

Page 86: LF Apostila

Que motivos o leitor imagina que o programador tenha levado em conta na definicao dafuncao devolveLivro, a exemplo da funcao zip, definida anteriormente, preferindo apresentara definicao para o padrao de lista vazia apos a definicao para o padrao de lista nao vazia, quandoo normal seria apresentar estes padroes na ordem inversa?

Exercıcio:

Modifique o banco de dados da Biblioteca anterior e as funcoes de acesso, de forma que:

• exista um numero maximo de livros que uma pessoa possa tomar emprestado,

• exista uma lista de palavras-chave associadas a cada livro, de forma que cada livro possaser encontrado atraves das palavras-chave a ele associadas, e

• existam datas associadas aos emprestimos, para poder detectar os livros com datas deemprestimos vencidas.

4.3 Compreensoes e expressoes ZF (Zermelo-Fraenkel)

As compreensoes, tambem conhecidas como expressoes ZF, sao devidas a Zermelo e Fraenkel erepresentam uma forma muito rica de construcao de listas. O domınio desta tecnica permiteao programador resolver muitos problemas de maneira simples e, em muitos casos, inusitada.A sintaxe das expressoes ZF e muito proxima da descricao matematica de conjuntos por in-tensionalidade, exprimindo determinadas propriedades. As diferencas se verificam apenas nossinais utilizados nas representacoes, mas a logica subjacente e a mesma. Vamos mostrar estassemelhancas atraves de exemplos e depois vamos formalizar sua sintaxe.

Vamos supor que ex = [2,4,7]. Usando ex, podemos construir ex1, a lista cujos elementossejam o dobro dos elementos de ex, da seguinte forma:

ex1 = [2*a | a<-ex]

Desta forma ex1 = [4,8,14]. Se quizermos encontrar a lista ex2 composta dos elementos deex que sejam pares, podemos declarar

ex2 = [a | a<-ex, a ’mod’ 2 == 0]

Neste caso, ex2 = [2,4].

A partir destes exemplos, podemos verificar que a sintaxe das expressoes ZF e realmentesimples. Formalmente ela e dada da seguinte forma:

[ e | q1, ..., qk] onde cada qi e um qualificador, que pode ter umas das seguintes formas:

1. pode ser um gerador do tipo p< −lExp, onde p e um padrao e lExp e uma expressao dotipo lista, ou

2. pode ser um teste do tipo bExp, uma expressao booleana.

Propriedades de uma expressao ZF

• Os geradores podem ser combinados com nenhuma, uma ou mais expressoes booleanas.Sendo ex a lista do Exemplo anterior, entao

[2*a | a <- ex, a ’mod’ 2 == 0, a > 3] = [8]

78

Page 87: LF Apostila

• Pode-se usar qualquer padrao a esquerda de < −somaPares :: [(Int, Int)] -> [Int]somaPares listadePares = [a + b | (a, b) <- listadePares]somaPares [(2,3), (4,5), (6,7)] = [5,9,13]

• Pode-se adicionar testes

novaSomaPares :: [(Int, Int)] -> [Int]novaSomaPares listadePares = [a + b | (a, b) <- listadePares, a < b]novaSomaPares [(2,3), (5,4), (7,6)] = [5]

• E possıvel colocar multiplos geradores e combinar geradores e testes.

• Uma expressao lExp ou bExp que aparece em um qualificador qi pode referenciar variaveisusadas nos padroes dos qualificadores q1 ate qi−1.

O algoritmo quicksort

Na secao 4.1.3, mostramos como o algoritmo de ordenacao por insercao direta pode ser imple-mentado em Haskell. Aqui sera mostrado como um outro algoritmo de ordenacao, quicksort,pode ser implementado, destacando-se a simplicidade como isto e feito. Suponhamos que o al-goritmo quicksort seja aplicado a uma lista de inteiros, ressaltando que ele tambem pode seraplicado a listas de qualquer tipo de dados, desde que estes dados possam ser comparados pelasrelacoes de ordem: maior, menor e igual.

O algoritmo quicksort utiliza o metodo de divisao e conquista em seu desenvolvimento. Emsua implementacao, escolhe-se um elemento, o pivot, e a lista a ser ordenada e dividida em duassub-listas: uma contendo os elementos menores ou iguais ao pivot e a outra contendo os elementosda lista que sejam maiores que o pivot. Neste ponto, o algoritmo e aplicado recursivamente aprimeira e a segunda sub-listas, concatenando seus resultados, com o pivot entre elas. A escolhado pivot, normalmente, e feita pelo elemento do meio da lista, na expectativa de que ele estejaproximo da media da amostra. No entanto, esta escolha e apenas estatıstica e, na realidade,pode-se escolher qualquer elemento da lista. Em nossa implementacao do quicsort em Haskell,escolhemos como pivot a cabeca da lista, por ser o elemento mais facil de ser obtido.

Vamos acompanhar a sequencia de operacoes na aplicacao do quicksort a lista [4,3,5,10].

quicksort [4,3,5,10]= quicksort [3] ++ [4] ++ quicksort [5,10]= (quicksort [ ] ++ [3] ++ quicksort [ ]) ++ [4] ++(quicksort [ ] ++ [5] ++ quicksort [10])

= ([ ] ++ [3] ++ [ ]) ++ [4] ++ ([ ] ++ [5] ++(quicsort [ ] ++ [10] ++ quicsort [ ]))

= [3] ++ [4] ++ ([5] ++ ([ ] ++ [10] ++ [ ]))= [3,4] ++ ([5] ++ [10])= [3,4] ++ [5,10]= [3,4,5,10]

Agora vamos definir formalmente o quicksort, usando expressoes ZF.

quicksort :: [t] -> [t]quicksort [ ] = [ ]quicksort (a : x) = quicksort [y | y <- x, y <= a] ++ [a] ++

quicksort [y | y <- x, y > a]

79

Page 88: LF Apostila

Esta definicao pode tambem ser feita usando definicoes locais, tornando-a mais facil de sercompreendida, da seguinte forma.

quicksort :: [t] -> [t]quicksort [ ] = [ ]quicksort (a : x) = quicksort menores ++ [a] ++ quicksort maiores

where menores = [y | y <- x, y <= a]maiores = [y | y <- x, y > a]

Nao e fantastica esta definicao? Sugiro ao leitor verificar a implementacao deste algoritmoutilizando alguma linguagem imperativa como C, C++ ou Java e observando as diferencas emfacilidade de entendimento e de implementacao.

Mais exemplos.

1. A funcao fazpares:

fazpares :: [t] -> [u] -> [(t,u)]fazpares l m = [ (a, b) | a <- l, b <- m]fazpares [1,2,3] [4,5] = [(1,4), (1.5), (2,4), (2,5), (3,4), (3,5)]

2. A funcao pares:

pares :: Int -> [(Int, Int)]pares n = [ (a, b) | a <- [1 .. n], b <- [1 .. a]]pares 3 = [ (1,1), (2,1), (2,2), (3,1), (3,2), (3,3)]

3. Os triangulos retangulos:

trianguloretangulo n = [ (a, b, c) | a <- [2 .. n], b <- [a+1 .. n],c <- [b+1 .. n], a * a + b * b == c * c]

trianguloretangulo 100 = [(3,4,5), (5,12,13), (6,8,10), ..., (65,72,97)]

Comentarios

No exemplo 1) deve ser observada a forma como as expressoes sao construıdas. O primeiroelemento escolhido, a, vem da lista l. Para ele, sao construıdos todos os pares possıveis comos elementos, b, que vem do outro gerador, a lista m. Assim toma-se o elemento 1 da lista[1,2,3] e formam-se os pares com os elementos 4 e 5 da lista [4,5]. Agora escolhe-se o segundoelemento da lista l, 2, e formam-se os pares com os elementos da lista m. Finalmente, repete-seeste processo para o terceiro elemento da lista l. Esta forma de construcao tem importanciafundamental, sendo responsavel pela construcao de listas potencialmente infinitas, um topicodescrito no proximo Capıtulo. Neste exemplo, tambem se nota que uma expressao ZF pode naoter qualquer expressao boolena.

No exemplo 2) deve-se notar a importancia que tem ordem em que os geradores sao colocados.Se o gerador de b viesse antes do gerador de a, ocorreria um erro.

No exemplo 3) tambem deve ser observada a ordem em que os geradores foram colocadospara que seja possıvel a geracao correta dos triangulos retangulos.

A funcao livrosEmprestados, definida no inıcio deste Capıtulo, pode ser re-definida daseguinte forma:

livrosEmprestados :: BancodeDados -> Pessoa -> [Livro]livrosEmprestados db fulano = [liv | (pes, liv) <- db, pes == fulano]

80

Page 89: LF Apostila

Exercıcios:

1. Re-implemente as funcoes de atualizacao do Banco de Dados para a Biblioteca, feita noinıcio do Capıtulo, usando compreensao de listas, em vez de recursao explıcita.

2. Como pode a funcao membro :: [Int] − > Int − > Bool ser definida usando compre-ensao de listas e um teste de igualdade?

Exemplo. Vamos agora mostrar um exemplo mais completo, baseado na referencia [35], quemostra algumas das possibilidades que as compreensoes oferecem. Seja um processador de textosimples que organiza um texto, identando-o pela esquerda, como por exemplo:

Maria gostava de bananas eestava apaixonadaporJoaquim e tomou veneno paramorrer.

Este pequeno trecho deve ser transformado em um texto mais organizado, ficando da seguinteforma:

Maria gostava de bananas e estava apaixonadapor Joaquim e tomou veneno para morrer.

Para isto, vamos construir algumas funcoes para realizar tarefas auxiliares. Inicialmente,devemos observar que uma palavra e uma sequencia de caracteres que nao tem espacos embranco dentro dela. Os espacos em branco sao definidos da seguinte forma:

espacoEmBranco :: [Char]espacoEmBranco = [’\n’, ’\t’, ’ ’]

Vamos definir a funcao pegaPalavra que, quando aplicada a uma string, retira a primeirapalavra desta string se a string nao iniciar com um espaco em branco. Assim pegaPalavra”bicho besta”= ”bicho” e pegaPalavra ” bicho”= porque a string e iniciada com umcaractere em branco.

Uma definicao para ela pode ser feita, usando a funcao pertence que verifica se um deter-minado caractere a pertence, ou nao, a uma string:

pertence :: Char -> [Char] -> Boolpertence _ [ ] = Falsepertence c (a:x)|c == a = True|otherwise = pertence c x

pegaPalavra :: String -> StringpegaPalavra [ ] = [ ]pegaPalavra (a:x)|pertence a espacoEmBranco = [ ]|otherwise = a : pegaPalavra x

Ja a funcao tiraPalavra, quando aplicada a uma string, retira a primeira palavra da stringe retorna a string restante, tendo como seu primeiro caractere o espaco em branco. Assim,tiraPalavra ”bicho feio”= ” feio”.

81

Page 90: LF Apostila

tiraPalavra :: String -> StringtiraPalavra [ ] = [ ]tiraPalavra (a : x)|pertence a espacoEmBranco = (a : x)|otherwise = tiraPalavra x

E necessario construir uma funcao que retire os espacos em branco da frente das palavras.Esta funcao sera tiraespaco que, aplicada a uma string iniciada com um ou mais espacos embranco, retorna outra string sem estes espacos em branco.

tiraEspaco :: String -> StringtiraEspaco [ ] = [ ]tiraEspaco (a : x)|pertence a espacoEmBranco = tiraEspaco x|otherwise = (a : x)

Resta agora formalizar como uma string, st, e dividida em palavras. Assumindo que st naoseja iniciada com espaco em branco, entao:

• a primeira palavra sera dada por pegaPalavra st,

• o restante sera feito dividindo-se a string que resulta da remocao da primeira palavra edo espaco em branco que a segue, ou seja, a nova divisao sera feita sobre tiraEspaco(tiraPalavra st).

type Palavra = StringdivideEmPalavras :: String -> [Palavra]divideEmPalavras st = divide (tiraEspaco st)

divide :: String -> [Palavra]divide [ ] = [ ]divide st = (pegaPalavra st) : divide (tiraEspaco (tiraPalavra st))

Vamos acompanhar a sequencia de operacoes da aplicacao divideEmPalavras ”bichobom”.

divideEmPalavras " bicho bom"= divide (tiraEspaco " bicho bom")= divide ‘‘bicho bom’’= (pegaPalavra "bicho bom") : divide (tiraEspaco (tiraPalavra "bicho bom"))= "bicho": divide (tiraEspaco " bom")= "bicho" : divide "bom"= "bicho" : (pegaPalavra "bom") : divide (tiraEspaco (tiraPalavra "bom"))= "bicho" : "bom" : divide (tiraEspaco [ ])= "bicho" : "bom" : divide [ ]= "bicho" : "bom" : [ ]= ["bicho", "bom"]

Agora e necessario tomar uma lista de palavras e transforma-la em uma lista de linhas, ondecada linha e uma lista de palavras, com um tamanho maximo (a linha). Para isto, vamos definiruma funcao que forme uma unica linha com um tamanho determinado.

82

Page 91: LF Apostila

type Linha = [Palavra]formaLinha :: Int -> [Palavra] -> Linha

Inicialmente, vamos admitir algumas premissas:

• Se a lista de palavras for vazia, a linha tambem sera vazia.

• Se a primeira palavra disponıvel for p, ela fara parte da linha se existir vaga para ela nalinha. O tamanho de p, length p, tera que ser menor ou igual ao tamanho da linha (tam).O restante da linha e construıdo a partir das palavras que restam, considerando uma linhade tamanho tam-(length p + 1).

• Se a primeira palavra nao se ajustar, a linha tem de ser vazia.

formaLinha tam [ ] = [ ]formaLinha tam (p:ps)|length p <= tam = p : restoDaLinha|otherwise = [ ]

wherenovoTam = tam - (length p + 1)restoDaLinha = formaLinha novoTam ps

Vamos acompanhar a sequencia de aplicacao da funcao:

formaLinha 20 ["Maria", "foi", "tomar", "banho", ...= "Maria" : formaLinha 14 ["foi", "tomar", "banho", ...= "Maria" : "foi": formaLinha 10 ["tomar", "banho" ...= "Maria" : "foi": "tomar" : formaLinha 4 ["banho", ...= "Maria" : "foi": "tomar" : [ ]= ["Maria","foi","tomar"]

Precisamos criar uma funcao, tiraLinha, que receba como parametros um tamanho queuma linha deve ter e uma lista de palavras e retorne esta lista de palavras sem a primeira linha.Esta funcao sera deixada como exercıcio, no entanto, indicamos seu tipo.

tiraLinha :: Int -> [Palavra] -> [Palavra]

Agora e necessario juntar as coisas. Vamos construir uma funcao que transforme uma listade palavras em uma lista de linhas. Primeiro ela forma a primeira linha, depois retira as palavrasdesta linha da lista original e aplica a funcao recursivamente a lista restante.

divideLinhas :: [Palavra] -> [Linha]divideLinhas [ ] = [ ]divideLinhas x = formaLinha tamLin x : divideLinhas (tiraLinha tamLin x).

Falta agora construir uma funcao que transforme uma string em uma lista de linhas, for-mando o novo texto identado a esquerda.

preenche :: String -> [Linha]preenche st = divideLinhas (divideEmPalavras st)

Finalmente deve-se juntar as linhas para que se tenha o novo texto, agora formando umastring identada a esquerda. Sera mostrada apenas o seu tipo, deixando sua definicao comoexercıcio.

83

Page 92: LF Apostila

juntaLinhas :: [Linha] -> String

Exercıcios

1. De uma definicao de uma funcao juntaLinha :: Linha − > String que transformauma linha em uma forma imprimıvel. Por exemplo, juntaLinha [”bicho”, ”bom”] =”bicho bom”.

2. Use a funcao juntaLinha do exercıcio anterior para definir uma funcao juntaLinhas ::[Linha] − > String que junta linhas separadas por ’\n’.

3. Modifique a funcao juntaLinha de forma que ela ajuste a linha ao tamanho tam, adici-onando uma quantidade de espacos entre as palavras.

4. Defina uma funcao estat :: String − > (Int, Int, Int) que aplicada a um texto retornao numero de caracteres, palavras e linhas do texto. O final de uma linha e sinalizado pelocaractere newline (‘\n’). Defina tambem uma funcao novoestat :: String − > (Int,Int, Int) que faz a mesma estatıstica sobre o texto, apos ser ajustado.

5. Defina uma funcao subst :: String − > String − > String − > String de forma quesubst velhaSub novaSub st faca a substituicao da sub-string velhaSub pela sub-stringnovaSub em st. Por exemplo, subst ”much” ”tall” ”How much is that?”= ”Howtall is that?” (Se a sub-string velhaSub nao ocorrer em st, o resultado deve ser st).

4.4 Funcoes de alta ordem

Provavelmente, a maioria dos leitores ja estejam familiarizados com a ideia de listas de listas,de dar nomes as listas ou de funcoes que adimitem listas como seus parametros. O que talvezpareca extranho para muitos e a ideia de listas de funcoes ou de funcoes que retornam outrasfuncoes com resultados. Esta e uma caracterıstica importante das linguagens funcionais, A ideiacentral e a de que as funcoes sao consideradas com os mesmos direitos que qualquer outro tipode dado, dizendo-se, corriqueiramente, que ”elas sao cidadas de primeira categoria”.

Vamos imaginar uma funcao twice que, quando aplicada a uma outra funcao, por exemplo,f, produza, como resultado, uma outra funcao que aplicada a seu argumento tenha o mesmoefeito da aplicacao da funcao f, duas vezes. Assim,

twice f x = f (f (x)) = (f . f) x

Como outro exemplo, vamos considerar a seguinte definicao em Haskell:

let suc = soma 1soma x = somax

where somax y = x + yin suc 3

O resultado desta aplicacao e 4. O efeito de soma x e criar uma funcao chamada somax,que adiciona x a algum numero natural, no caso, y. Desta forma, suc e uma funcao que adicionao numero 1 a um numero natural qualquer.

A expressao f(g(x)), normalmente, e escrita pelos matematicos como (f.g)(x), onde o .(ponto) e o operador de composicao de funcoes. Esta notacao e importante porque separa aparte composta apenas por funcoes da parte dos argumentos. O operador de composicao e umtipo de funcao, cujos argumentos sao duas funcoes e o resultado e tambem uma funcao.

Como mais um exemplo, vamos considerar a definicao

84

Page 93: LF Apostila

let quad = compoe (quadrado, sucessor)quadrado x = x * xsucessor x = x + 1compoe (f,g) = h where h x = f(g(x))

in quad 3

A resposta a esta aplicacao e 16. O interesse maior aqui esta na definicao da funcao compoe.Ela toma um par de parametros, f e g (ambos funcoes) e retorna uma outra funcao, h, cujoefeito de sua aplicacao e a composicao das funcoes f e g.

A partir destes exemplos, podemos caracterizar duas ferramentas importantes, a saber:

1. Um novo mecanismo para passar dois ou mais parametros para uma funcao. Apesar dafuncao soma ter sido declarada com apenas um parametro, poderıamos chama-la, porexemplo, como soma 3 4, que daria como resultado 7. Isto significa que soma 3 temcomo resultado uma outra funcao que, aplicada a 4, da como resultado o valor 7.

2. Um mecanismo de aplicacao parcial de funcoes. Isto quer dizer que, se uma funcao fordeclarada com n parametros, podemos aplica-la a m destes parametros, mesmo que mseja menor que n.

As funcoes de alta ordem sao usadas de forma intensa em programas funcionais, permitindoque computacoes complexas sejam expressas de forma simples. Vejamos algumas destas funcoes,muito utilizadas na pratica da programacao funcional.

4.4.1 A funcao map

Um padrao de computacao que e explorado como funcao de alta ordem envolve a criacao de umalista (listaNova) a partir de uma outra lista (listaVelha), onde cada elemento de listaNovatem o seu valor determinado atraves da aplicacao de uma funcao a cada elemento de listaVelha.

Suponhamos que se deseja transformar uma lista de nomes em uma nova lista de tuplas, emque cada nome da primeira lista e transformado em uma tupla, onde o primeiro elemento seja oproprio nome e o segundo seja a quantidade de caracteres do nome. Para isso, sera construıdauma funcao listaTupla de forma que

listaTupla ["Dunga","Constantino"] = [("Dunga",5),("Constantino",11)]

Antes vamos criar a funcao auxiliar tuplaNum que, aplicada a um nome, retorna a tuplaformada pelo nome e a quantidade de caracteres do nome.

tuplaNum :: [Char] -> ([Char], Int)tuplaNum n = (n, length n)

Agora a funcao listaTupla pode ser definida da seguinte maneira:

listaTupla :: [String] -> [(String, Int)]listaTupla [ ] = [ ]listaTupla (a : x) = (tuplaNum a) : listaTupla x

Vamos agora, supor que se deseja transformar uma lista de inteiros em uma outra lista deinteiros, onde cada valor inteiro da primeira lista seja transformado em seu dobro, ou seja,

dobraLista [3, 2, 5] = [6, 4, 10]

85

Page 94: LF Apostila

Como na definicao da funcao anterior, vamos construir a funcao auxiliar, dobra, da seguinteforma:

dobra :: Int -> Intdobra x = 2*x

dobraLista :: [Int] -> [Int]dobraLista [ ] = [ ]dobraLista (a : x) = (dobra a) : dobraLista x

Analisando as definicoes listaTupla e dobraLista, observamos a presenca de um padraoque e o de aplicar uma funcao a cada elemento da lista, ou seja, as duas funcoes percorrem aslistas aplicando uma funcao a cada um de seus elementos.

Uma outra opcao e construir uma funcao de alta ordem, destinada a realizar esta varre-dura, aplicando uma funcao a cada elemento da lista. A funcao a ser aplicada e passada comoparametro para a funcao de alta ordem. Neste caso, a funcao de alta ordem e chamada de “ma-peamento”, simbolizado pela funcao pre-definida map, definida em Haskell da seguinte forma:

map :: (t -> u) -> [t] -> [u]map f [ ] = [ ]map f (a : x) = (f a) : (map f x)

Assim, as definicoes anteriores de listaTupla e dobraLista podem ser re-definidas da se-guinte forma:

listaTupla :: [String] -> [(String, Int)]listaTupla x = map tuplaNum x

dobraLista :: [Int] -> [Int]dobraLista x = map dobra x

Vejamos mais alguns exemplos:

duplica, triplica :: Int -> Intduplica n = 2 * ntriplica n = 3 * n

duplicaLista, triplicaLista :: [Int] -> [Int]duplicaLista l = map duplica ltriplicaLista l = map triplica l

map duplica [4,5] = duplica 4 : map duplica [5]= 8 : map duplica [5]= 8 : (duplica 5 : map duplica [ ])= 8 : (10 : map duplica [ ])= 8 : (10 : [ ])= 8 : [10]= [8,10]

a utilizacao de funcoes de alta ordem se justifica, baseando-se nas seguintes premissas:

86

Page 95: LF Apostila

• E mais facil entender a definicao, porque torna claro que se trata de um mapeamento, porcausa da funcao map. Precisa apenas entender a funcao mapeada.

• E mais facil modificar as definicoes das funcoes a serem aplicadas, se isto for necessario.

• E mais facil reutilizar as definicoes.

Exemplo: as funcoes de analise de vendas, mostradas no Capıtulo anterior, foram definidaspara analisar uma funcao fixa: vendas. Agora, a funcao totalVendas pode ser dada por

totalVendas n = map vendas n

Muitas outras funcoes podem ser definidas usando map. Por exemplo,

somaQuad :: Int -> IntsomaQuad n = map quad n

quad :: Int -> Intquad x = x * x

Exercıcios

1. De definicoes de funcoes que tome uma lista de inteiros l e

• retorne a lista dos quadrados dos elementos de l,

• retorne a soma dos quadrados dos elementos de l e

• verifique se todos os elementos da lista sao positivos.

2. Escreva definicoes de funcoes que

• de o valor mınimo de uma funcao aplicada a uma lista de 0 a n,

• teste se os valores de f sobre as entradas 0 a n sao todas iguais.

• teste se todos os valores de f aplicada as entradas de 0 a n sao maiores ou iguais azero e

• teste se os valores f 0, f 1 ate f n estao em ordem crescente.

3. Estabeleca o tipo e defina uma funcao trwice que toma uma funcao de inteiros parainteiros e um inteiro e retorna a funcao aplicada a entrada tres vezes. Por exemplo, coma funcao triplica e o inteiro 4 como entradas, o resultado e 108.

4. De o tipo e defina uma funcao iter de forma que iter n f x = f ( f ( f . . . (f x) . ..)), onde f ocorre n vezes no lado direito da equacao.

Por exemplo, devemos ter: iter 3 f x = f ( f ( f x )) e iter 0 f x = x.

5. Usando iter e duplica, defina uma funcao que aplicada a n retorne 2n.

87

Page 96: LF Apostila

4.4.2 Funcoes anonimas

Ja foi visto, e de forma entatica, que, nas linguagens funcionais, as funcoes podem ser usadascomo parametros para outras funcoes. No entanto, seria um desperdıcio definir uma funcao queso pudesse ser utilizada como parametro para outra funcao. Isto implicaria que a funcao sofosse utilizada neste caso e nem um outro mais. No caso da secao anterior, as funcoes dobrae tuplaNum, possivelmente, so sejam utilizadas como argumentos das funcoes dobraLista elistaTupla.

Uma forma de declarar funcoes para serem utilizadas apenas localmente e usar a clausulawhere. Por exemplo, dado um inteiro n, vamos definir uma funcao que retorne uma outrafuncao de inteiro para inteiro que adiciona n a seu argumento.

somaNum :: Int -> (Int -> Int)somaNum n = h where h m = n + m

Quando se necessita especificar um valor, normalmente, se declara um identificador para isto.Esta tambem tem sido a forma utilizada com as funcoes, ou seja, declara-se uma funcao comum nome e, quando necessaria, e referenciada atraves de seu nome. Uma alternativa, possıvelem Haskell, consiste em declarar uma funcao apenas no ponto de chamada. Estas funcoes saoas “funcoes anonimas”. As funcoes anonimas se baseiam na notacao do λ-calculo, visto noCapıtulo 2. Esta forma e mais compacta e mais eficiente. A funcao somaNum, definida acimausando a clausula where, pode ser escrita, de forma anonima, da seguinte forma:

\m -> n + m

As funcoes anonimas permitem um acesso direto a funcoes, sem identificadores. Nese caso,as funcoes dobraLista e listaTupla podem ser definidas da seguinte forma:

dobraLista x = map (\n -> 2*n) xlistaTupla x = map (\n -> (n, length n)) x

Vamos agora analisar como a sintaxe de uma funcao anonima e feita. Uma definicao anonimae dividida em duas partes: uma antes da flexa e a outra depois dela. Estas duas partes tem asseguintes interpretacoes:

• antes da flexa vem os argumentos (neste caso, apenas n) e

• depois da flexa vem o resultado.

A barra invertida no inıcio (\) indica que se trata de uma funcao anonima. A \ e o caracteremais parecido com a letra grega λ, usada no λ-calculo.

Vejamos a funcao comp2, mostrada graficamente na Figura 4.1. Na realidade, trata-se deuma funcao g que recebe como entrada dois argumentos, no caso f x e f y, que sao os resultadosdas aplicacoes da funcao f aos argumentos x e y, ou seja g (f x) (f y).

A definicao de comp2 e

comp2 :: (a -> b) -> (b -> b -> c) -> (a -> a -> -c)comp2 f g = (\x y -> g (f x) (f y))

Para se adicionar os quadrados de 5 e 6, podemos escrever

comp2 quad soma 5 6

88

Page 97: LF Apostila

x

y

f

f

g (f x) (f y)

comp2 f g

Figura 4.1: Forma grafica da funcao anonima comp2.

onde quad e soma tem significados obvios. De forma geral, sendo f definida por

f x y z = resultado

entao f pode ser definida anonimamente por

\x y z -> resultado

As funcoes fold e foldr

Uma outra funcao de alta ordem, tambem de grande utilizacao em aplicacoes funcionais, e afuncao fold, usada na combinacao de ıtens (folding). Ela toma como argumentos uma funcaode dois argumentos e a aplica aos elementos de uma lista. O resultado e um elemento do tipodos elementos da lista. Vamos ver sua definicao formal e exemplos de sua aplicacao.

fold :: (t -> t -> t) -> [t] -> tfold f [a] = afold f (a:b:x) = f a (fold f (b:x))

Exemplos:

fold (||) [False, True, False] = (||) False (fold (||) [True, False])= (||) False ((||) True (fold (||) [False])= (||) False ((||) True False)= (||) False True= True

fold (++) [”Chico”, ”Afonso”, , ”!”] = ”Chico Afonso!”fold (*) [1..6] = 720 (Verifique!)

No entanto, existe um pequeno problema na definicao de fold. Se ela for aplicada a umafuncao e uma lista vazia ocorrera um erro. Para resolver este impasse, foi pre-definida, emHaskell, uma outra funcao para substituir fold, onde este tipo de erro seja resolvido. Esta e afuncao foldr, definida da seguinte forma:

foldr :: (t -> u -> u) -> u -> [t] -> ufoldr f s [ ] = sfoldr f s (a : x) = f a (foldr f s x)

Vamos verificar como algumas funcoes sao definidas usando foldr.

Exemplos:

89

Page 98: LF Apostila

concat :: [[t]] -> [t] and :: [Bool] -> Boolconcat xs = foldr (++) [ ] xs and bs = foldr (&&) True bs

rev :: [t] -> [t] stick :: t -> [t] -> [t]rev l = foldr stick [] l stick a x = x ++ [a]

A funcao filter

A funcao filter e uma outra funcao de alta ordem, pre-definida em todas as linguagens funcionais,de bastante utilizacao. Resumidamente, ela escolhe dentre os elementos de uma lista, aquelesque tem uma determinada propriedade. Vejamos alguns exemplos:

1. filter ehPar [2,3,4] = [2,4].

2. Um numero natural e perfeito se a soma de seus divisores, incluindo o numero 1, for oproprio numero. Por exemplo, 6 e o primeiro numero natural perfeito, porque 6 = 1+2+3.Vamos definir uma funcao que mostra os numeros perfeitos entre 0 e m.

divide :: Int -> Int -> Booldivide n a = n ‘mod‘ a == 0

fatores :: Int -> [Int]fatores n = filter (divide n) [1..(n ‘div‘ 2)]

perfeito :: Int -> Boolperfeito n = sum (fatores n) == n

perfeitos m = filter perfeito [0..m]

E se quizermos os primeiros m numeros perfeitos?

3. A lista de todos os numeros pares maiores que 113 e menores ou iguais a 1000, que sejamperfeitos: filter perfeito [y | y < − [114 .. 1000], ehPar y].

4. Selecionando elementos: filter digits “18 Marco 1958” = “181958”

A definicao formal de filter e:

filter :: (t -> Bool) -> [t] -> [t]filter p [ ] = [ ]filter p (a : x) ou filter p x = [a | a <- x, p a]|p a = a : filter p x|otherwise = filter p x

4.5 Polimorfismo

Uma caracterıstica muito importante das linguagens funcionais e que suas definicoes podemser polimorficas, um mecanismo que aumenta o poder de expressividade de qualquer lingua-gem. Polimorfismo e uma das caracterısticas responsaveis pela alta produtividade de software,proporcionada pelo aumento da reusabilidade.

90

Page 99: LF Apostila

Polimorfismo e a capacidade de aplicar uma mesma funcao a varios tipos de dados, represen-tados por um tipo variavel. Nao deve ser confundido com sobrecarga, denominada por muitospesquisadores como polimorfismo ad hoc, que consiste na aplicacao de varias funcoes com omesmo nome a varios tipos de dados. Haskell permite os dois tipos de polimorfismo, sendo quea sobrecarga e feita atraves de um mecanismo engenhoso chamado de type class, um tema aser estudado no proximo Capıtulo.

A funcao length e pre-definida em Haskell, da seguinte maneira:

length :: [t] -> Intlength [ ] = 0length (a : x) = 1 + length x

Esta funcao tem um tipo polimorfico porque pode ser aplicada a qualquer tipo de listahomogenea. Em sua definicao nao existe qualquer operacao que exija que a lista parametro sejade algum tipo particular. A unica operacao que esta funcao faz e contar os elementos de umalista, seja ela de que tipo for.

Ja a funcao

quadrado :: Int -> Intquadrado x = x * x

nao pode ser polimorfica porque so e aplicavel a elementos onde a operacao de multiplicacao (*)seja possıvel. Por exemplo, nao pode ser aplicada a strings, nem a valores booleanos.

4.5.1 Tipos variaveis

Quando uma funcao tem um tipo envolvendo um ou mais tipos variaveis, diz-se que ela temum tipo polimorfico. Por exemplo, ja vimos anteriormente que a lista vazia e um elemento dequalquer tipo de lista, ou seja, [ ] esta na intersecao dos tipos [Int], [Bool] ou [Char], etc. Paraexplicitar esta caracterıstica denota-se que [ ] :: [t], sendo t e uma variavel que pode assumirqualquer tipo. Assim, cons tem o tipo polimorfico (:) :: t − > [t] − > [t].

As seguintes funcoes, algumas ja definidas anteriormente, tem os tipos:

length :: [t] -> Int(++) :: [t] -> [t] -> [t]rev :: [t] -> [t]id :: t -> tzip :: [t] -> [u] -> [(t, u)]

4.5.2 O tipo mais geral

Alguma dificuldade pode surgir nas definicoes dos tipos das funcoes quando elas envolvem tiposvariaveis, uma vez que podem existir muitas instancias de um tipo variavel. Para resolver estedilema, e necessario que o tipo da funcao seja o tipo mais geral possıvel.

Um tipo w de uma funcao f e ”o tipo mais geral” de f se todos os tipos de f forem instanciasde w.

O tipo [t] − > [t] − > [(t, t)] e uma instancia do tipo da funcao zip, mas nao e o tipomais geral, porque [Int] − > [Bool] − > [(Int, Bool)] e um tipo para zip, mas nao e umainstancia de [t]− > [t]− > [(t, t)].

Exemplos de algumas funcoes polimorficas.

91

Page 100: LF Apostila

rep :: Int -> t -> [t]rep 0 ch = [ ]rep n ch = ch : rep (n - 1) ch

fst :: (t, u) -> t snd :: (t, u) -> ufst (x, _) = x snd (_, y) = y

head :: [t] -> t tail :: [t] -> [t]head (a : _) = a tail (_ : x) = x

Mas qual a vantagem de se ter polimorfismo? A resposta vem da Engenharia de Software ese baseia nos seguintes fatos:

• definicoes mais gerais implicam em maior chance de reutilizacao e

• em linguagens nao polimorficas, as funcoes devem ser re-definidas para cada novo tipo.Isto implica em ineficiencia e inseguranca.

Exercıcios

1. Defina uma funcao concat onde concat [e1, ..., ek] = e1 ++ ... ++ ek. Qual o tipode concat?

2. Defina uma funcao unZip que transforma uma lista de pares em um par de listas. Qualo seu tipo?

3. Defina uma funcao last :: [t] − > t que retorna o ultimo elemento de uma lista naovazia. Defina tambem init :: [t] − > [t] que retorna todos os elementos de uma lista comexcecao do ultimo elemento da lista.

4. Defina funcoes tome, tire :: Int − > [t] − > [t] onde tome n l retorna os n primeiroselementos da lista l e tire n l retira os n primeiros elementos da lista l.

4.6 Inducao estrutural

Ja vimos formas de se provar propriedades em Haskell. No entanto, quando estas propriedadesenvolvem listas, existe uma forma especıfica de serem provadas, que e a inducao estrutural. Pode-se dizer que inducao estrutural e o metodo de inducao matematica aplicado as listas finitas. Aslistas infinitas nao sao tratadas, uma vez que elas nao sao estruturas modelaveis na Computacao.Os computadores sao maquinas com memorias limitadas, apesar de poderem ser grandes, massao finitas. Apesar de alguns autores se referirem as listas infinitas, o que realmente eles sereferem sao as listas potencialmente infinitas, que sao implementadas em Haskell atraves deum mecanismo de avaliacao lazy, um topico a ser visto mais adiante. Deve ser lembrado que alista vazia, [ ], e uma lista finita e a lista nao vazia, (a:x), e uma lista finita, se a lista x forfinita.

Esquema de prova

O esquema de provas mostrado a seguir e devido a Simon Thompson [35]. Apesar de muitoformal, ele deve ser seguido, principalmente, por iniciantes, que ainda nao tem experiencia comprovas de programas. Para estes usuarios, e recomendavel utiliza-lo como forma de treinamento.

92

Page 101: LF Apostila

Muitas pessoas tendem a querer chegar a conclusao de uma prova forcando situacoes, sem aargumentacao adequada e este tipo de vıcio ha que ser evitado, a qualquer custo. A falta dedomınio nesta area pode levar o usuario a conclusoes equivocadas.

Outro erro, comumente cometido por algumas pessoas nao afeitas a provas matematicas,consiste em realizar provas sem se importar com as conclusoes das mesmas. A conclusao e parteıntegrante do esquema de provas e, portanto, indispensavel. Ela e o objetivo da prova. Semela nao existe razao para todo um esforco a ser despendido nas fases anteriores. A conclusaorepresenta o desfecho de uma prova e representa a formalizacao de uma proposicao que passa aser verdadeira e pode ser utilizada em qualquer etapa de uma computacao.

O esquema de provas deve se constituir nos seguintes estagios:

Estagio 0: escrever o objetivo da prova informalmente,Estagio 1: escrever o objetivo da prova formalmente,Estagio 2: escrever os sub-objetivos da prova por inducao:

P([ ]) eP(a : x), assumindo P(x)

Estagio 3: provar P([ ])Estagio 4: provar P(a : x), lembrando que PODE e DEVE usar P(x).

Vejamos agora, dois exemplos completos de esquemas de provas que envolvem todos osestagios enumerados anteriormente.

Exemplo 1. Dadas as definicoes a seguir:

somaLista [ ] = 0 (1)somaLista (a : x) = a + somaLista x (2)

dobra [ ] = [ ] (3)dobra (a : x) = (2 * a) : dobra x (4)

Provar que

• Estagio 0: o dobro da soma dos elementos de uma lista e igual a soma dos elementos dalista formada pelos dobros dos elementos da lista anterior.

• Estagio 1: somaLista (dobra x) = 2 * somaLista x (5)

• Estagio 2:

– sumList (dobra [ ]) = 2 * somaLista [ ] (6)

– somaLista (dobra (a : x)) = 2 * somaLista (a : x) (7)assumindo que somaLista (dobra x) = 2 * somaLista x (8)

• Estagio 3: Caso base:

lado esquerdo do caso base: lado direito do caso basesomaLista (dobra [ ]) 2 * somaLista [ ]= somaLista [ ] por (3) =2 * 0 por (1)= 0 por (1) =0 pela aritmetica.

Assim, a assertiva (6) e valida.

• Estagio 4: Passo indutivo:

93

Page 102: LF Apostila

lado esquerdo do passo indutivo:somaLista (dobra (a : x))= somaLista (2 * a : dobra x) por (4)= 2 * a + somaLista (dobra x) por (2)= 2 * a + 2 * somaLista x pela hipotese de inducao= 2 * (a + somaLista x) pela distributividade de *.

lado direito do passo indutivo:2 * somaLista (a : x)= 2 * (a + somaLista x) por (2).

Assim, a assertiva (7) e valida.

Conclusao: como a assertiva (5) e valida para o caso base e para o passo indutivo, entao ela evalida para todas as listas finitas.

Exemplo 2. Associatividade de append.

• Estagio 0: a funcao ++ e associativa.

• Estagio 1: Dadas as definicoes:

[ ] ++ v = v (1)(a : x) ++ v = a : (x ++ v) (2)

x ++ (y ++ z) = (x ++ y) ++ z

• Estagio 2:

– caso base: [ ] ++ (y ++ z) = ([ ] ++ y) ++ z (3)

– passo indutivo: (a : x) ++ (y ++ z) = ((a : x) ++ y) ++ z (4) assumindoque x ++ (y ++ z) = (x ++ y) ++ z (5)

• Estagio 3:

caso base: lado esquerdo caso base: lado direito[ ] ++ (y ++ z) ([ ] ++ y) ++ z= y ++ z por (1) = y ++ z por (1)

Como o lado esquerdo e o lado direito do caso base sao iguais, entao a propriedade e validapara ele.

• Estagio 4:

passo indutivo: lado esquerdo(a : x) ++ (y ++ z)= a : (x ++ (y ++ z)) por (2)

passo indutivo: lado direito((a : x) ++ y) ++ z= (a : (x ++ y)) ++ z por (2)= a : ((x ++ y) ++ z) por (2)= a : (x ++ (y ++ z)) pela hipotese de inducao.

Como o lado esquerdo e o lado direito do passo indutivo sao iguais, entao a propriedade evalida para ele.

94

Page 103: LF Apostila

Conclusao: como a assertiva e valida para o caso base e para o passo indutivo, entao ela everdadeira para todas as listas finitas.

Exercıcios

1. Prove que, para todas as listas finitas x, x ++ [ ] = x.

2. Tente provar que x ++ (y ++ z) = (x ++ y) ++ z usando inducao estrutural sobrez. Ha alguma coisa esquisita com esta prova? O que?

3. Prove que, para todas as listas finitas x e y, sumList (x ++ y) = sumList x +sumList y e que sumList (x ++ y) = sumList (y ++ x).

4. Mostre que, para todas as listas finitas x e y,double (x ++ y) = double x ++ double y elength (x ++ y) = length x + length y.

5. Prove por inducao sobre x que, para todas as listas finitas x,sumList (x ++ (a : y)) = a + sumList (x ++ y).

6. Prove, usando ou nao o exercıcio anterior, que para todas as listas finitas x,sumList (double x) = sumList (x ++ x).

4.7 Composicao de funcoes

Uma forma simples de estruturar um programa e construı-lo em etapas, uma apos outra, ondecada uma delas pode ser definida separadamente. Em programacao funcional, isto e feito atravesda composicao de funcoes, uma propriedade matematica so implementada nestas linguagens. Asua sintaxe obedece aos princıpios basicos empregados na matematica e aumenta, enormemente,a expressividade do programador. A funcao preenche foi definida anteriormente da seguinteforma:

preenche :: String -> [Linha]preenche st = divideLinhas (divideEmPalavras st)

divideEmPalavras :: String -> [Palavra]divideLinhas :: [Palavra] -> [Linha]

preenche pode ser re-escrita como: preenche = divideLinhas . divideEmPalavras

Esta forma de codificacao torna a composicao explıcita sem a necessidade de aplicar cadalado da igualdade a um argumento. Da Matematica herdamos a notacao de “.”, onde

(f . g) x = f ( g x )

Como e sabido da Matematica, nem todo par de funcoes pode ser composto. O tipo da saıdada funcao g tem de ser o mesmo tipo da entrada da funcao f. O tipo de . e: ( . ) :: (u − >v) − > (t − > u) − > (t − > v).

A composicao e associativa, ou seja, f . (g . h) = (f . g) . h, que deve ser interpretadocomo “faca h, depois faca g e finalmente faca f”.

95

Page 104: LF Apostila

4.7.1 Composicao avancada

A ordem em f . g e importante (faca g e depois f). Podemos fazer a composicao ter o sentidodas acoes propostas, usando a composicao avancada. Deve ser lembrado que esta forma eapenas para fins de apresentacao, uma vez que a definicao, como sera vista, e feita em funcaoda composicao e nada aumenta em expressividade. A composicao avancada sera denotada por> . > indicando a sequencializacao da aplicacao das funcoes, uma vez que, na composicao, elae realizada na ordem inversa de aparencia no texto. A definicao de > . > e feita da seguinteforma:

infixl 9 >.>(>.>) :: (t -> u) -> (u -> v) -> (t -> v)g >.> f = f . g

A funcao preenche, definida anteriormente, pode ser re-definida usando composicao avancada,da seguinte forma:

preenche = divideEmPalavaras > . > divideLinhas

Deve-se ter o cuidado de observar que f . g x e diferente de (f . g) x porque a aplicacaode funcoes tem prioridade sobre a composicao. Por exemplo, succ . succ 1 resultara em erroporque succ 1 sera realizada primeiro e retornara um inteiro (2), fazendo com que a composicaodo primeiro succ seja feita com um numero inteiro e nao com uma funcao. Neste caso, osparenteses devem ser utilizados para resolver ambiguidades.

4.7.2 Esquema de provas usando composicao

Seja a funcao twice f = f . f. Assim

(twice succ) 12= (succ . succ) 12 --pela definicao de twice= succ (succ 12) --pela definicao de .= succ 13 = 14

Pode-se generalizar twice, indicando um parametro que informe quantas vezes a funcao deveser composta com ela propria:

iter :: Int -> (t -> t) -> (t -> t)iter 0 f = iditer n f = f >.> iter (n - 1) f

Por exemplo, podemos definir 2n como iter n duplica e vamos mostrar a sequencia deoperacoes para n=2.

iter 2 duplica = duplica >.> iter 1 duplica= duplica >.> (duplica >.> iter 0 f)= duplica >.> (duplica >.> id))= duplica >.> duplica= twice duplica

Nesta definicao, usamos o fato de que f . id = f. Estamos tratando de uma nova especie deigualdade, que e a igualdade de duas funcoes. Mas como isto pode ser feito? Para isto, devemos

96

Page 105: LF Apostila

examinar como os dois lados se comportam quando os aplicamos a um mesmo argumento x.Entao,

(f . id) x= f (id x) pela definicao de composicao= f x pela definicao de id.

Isto significa que para um argumento x, qualquer, as duas funcoes se comportam exatamenteda mesma forma.

Vamos fazer uma pequena discussao sobre o que significa a igualdade entre duas funcoes.Existem dois princıpios que devem ser observados quando nos referimos a igualdade de funcoes.Sao eles:

• Princıpio da extensionalidade : “Duas funcoes, f e g, sao iguais se elas produziremexatamente os mesmos resultados para os mesmos argumentos”.

• Princıpio da intencionalidade : “Duas funcoes, f e g sao iguais se tiverem as mesmasdefinicoes”.

Se estivermos interessados nos resultados de nossos programas, tudo o que nos interessasao os valores dados pelas funcoes e nao como estes valores sao encontrados. Em Haskell,devemos usar extensionalidade quando estivermos interessados no comportamento das funcoes.Se estivermos interessados na eficiencia ou outros aspectos de desempenho de programas devemosusar a intencionalidade.

Exercıcios

1. Mostre que a composicao de funcoes e associativa, ou seja, ∀ f, g e h, f . (g . h) = (f .g) . h

2. Prove que ∀n ∈ Z+, iter n id = id.

3. Duas funcoes f e g sao inversas se f . g = id e g . f = id. Prove que as funcoes currye uncurry, definidas a seguir, sao inversas.

curry :: ((t, u) -> v) -> (t -> u -> v)curry f (a, b) = f a b

uncurry :: (t -> u -> v) -> ((t, u) -> v)uncurry g a b = g (a, b)

Exemplo: Seja a definicao de map e da composicao de funcoes dadas a seguir. Mostre quemap (f. g) x = (map f . map g) x

map f [ ] = [ ] (1)map f (a : x) = f a : map f x (2)(f . g) x = f (g x) (3)

Prova:

Caso base: a lista vazia, [ ]:

Lado esquerdo Lado direitomap (f . g) [ ] = [ ] por (1) (map f . map g) [ ]

= map f (map g [ ]) por (3)=map f [ ] por (1)=[ ] por (1)

97

Page 106: LF Apostila

Passo indutivo: (a : x)

Lado esquerdo Lado direitomap (f . g) (a : x) (map f . map g)(a:x)=(f . g) a : map (f . g) x (2) = map f (map g (a:x)) (3)=f (g a) : map (f . g) x (3) =map f (g a) : map f (map g x) (2)=f (g a) : (map f . map g) x (hi) =f (g a) : (map f . map g) x (3)

Conclusao. Como a propriedade e valida para a lista vazia e para a lista nao vazia, entao elae valida para qualquer lista homogenea finita.

Exercıcio: Prove que para todas as listas finitas l e funcoes f, concat (map (map f) l) =map f (concat l).

4.8 Aplicacao parcial

Uma caracterıstica importante de Haskell e que proporciona uma forma elegante e poderosa deconstrucao de funcoes e a avaliacao parcial que consiste na aplicacao de uma funcao a menosargumentos que ela realmente precisa. Por exemplo, seja a funcao multiplica que retorna oproduto de seus argumentos:

multiplica :: Int -> Int -> Intmultiplica a b = a * b

Esta funcao foi declarada para ser usada com dois argumentos. No entanto, ela pode serchamada como multiplica 2. Esta aplicacao retorna uma outra funcao que, aplicada a umargumento b, retorna o valor 2*b. Esta caracterıstica e o resultado do seguinte princıpio emHaskell: “uma funcao com n argumentos pode ser aplicada a r argumentos, onde r ≤ n”. Comoexemplo, a funcao dobraLista pode ser definida da seguinte forma:

dobraLista :: [Int] -> [Int]dobraLista = map (multiplica 2)

onde multiplica 2 e uma funcao de inteiro para inteiro, a aplicacao de multiplica a um deseus argumentos, 2, em vez de ser aplicada aos dois. map (multiplica 2) e uma funcao dotipo [Int] − > [Int], dada pela aplicacao parcial de map.

Como e determinado o tipo de uma aplicacao parcial? Pela regra do cancelamento: “se umafuncao f tem o tipo t1− > t2− > ... − > tn− > t e e aplicada aos argumentos e1 :: t1, e2::t2,... , ek :: tk, com k ≤ n, entao o tipo do resultado e dado pelo cancelamento dos tipos t1 atetk, dando o tipo tk+1− > tk+2 − > ... −→ tn −→ t.”

Por exemplo,

multiplica 2 :: Int -> Intmultiplica 2 3 :: IntdobraLista :: [Int] -> [Int]dobraLista [2,3,5] :: [Int]

Mas afinal, quantos argumentos tem realmente uma funcao em Haskell? Pelo exposto, aresposta correta a esta questao e 1 (UM).

Isto significa que uma funcao do tipo Int − > Int − > Int e do mesmo tipo que Int− > (Int − > Int). Neste ultimo caso, esta explıcito que esta funcao pode ser aplicada a umargumento inteiro e o seu resultado e uma outra funcao que recebe um inteiro e retorna outrointeiro. Exemplificando,

98

Page 107: LF Apostila

multiplica :: Int -> Int -> Int ou multiplica :: Int -> (Int -> Int)multiplica 4 :: Int -> Intmultiply 4 5 :: Int

Associatividade:

A aplicacao de funcao e associativa a esquerda, ou seja:

f a b = (f a) b

enquanto a flexa e associativa pela direita:

t − > u − > v = t − > (u − > v).

4.8.1 Secao de operadores

Uma decorrencia direta das aplicacoes parciais que representa uma ferramenta poderosa e ele-gante em algumas linguagens funcionais e, em particular, em Haskell, sao as secoes de operadores.As secoes sao operacoes parciais, normalmente relacionadas com as operacoes aritmeticas. Nasaplicacoes, elas sao colocadas entre parenteses. Por exemplo,

• (+2) e a funcao que adiciona algum argumento a 2,

• (2+) a funcao que adiciona 2 a algum argumento,

• (>2) a funcao que retorna True se um inteiro for maior que 2,

• (3:) a funcao que coloca o inteiro 3 na cabeca de uma lista,

• (++”\n”) a funcao que coloca o caractere ’\n’ ao final de uma string.

Uma secao de um operador op coloca o argumento no lado que completa a aplicacao. Porexemplo, (op a) b = b op a e (a op) b = a op b.

Exemplos:

• map (+1) > . > filter ( >0).

• dobra = map (*2).

• pegaPares = filter (( ==0) . (‘mod‘2)).

• A funcao inquilinos, definida no inıcio deste Capıtulo, normalmente, e escrita da seguinteforma:inquilinos db pes = map snd (filter ehPes db)

whereehPes (p, b) = (p == pes).

No entanto, ela pode ser escrita em forma de secao da seguinte maneira, o que a tornamuito mais elegante.inquilinos db pes = map snd (filter ((==pes) . fst) db).

4.8.2 Currificacao

Este nome tambem foi cunhado em homenagem a Haskell Brooks Curry, por sua pesquisa nalogica e no λ-calculo. Na realidade, Schonfinkel foi o pioneiro, mas Haskell foi quem mais

99

Page 108: LF Apostila

utilizou esta propriedade em suas pesquisas: “nas linguagens funcionais, uma funcao de aridaden e equivalente a n funcoes de aridade 1”.

Exemplo.

Este e mais um exemplo que mostra o poder de expressividade de Haskell. Trata-se da criacaode um ındice remissivo, encontrado na maioria dos livros tecnicos. Este exemplo e baseado nolivro de Simon Thompson [35]. Algumas funcoes ja foram definidas anteriormente, no entantoelas serao novamente definidas aqui para evitar ambiguidades. Vamos mostrar, inicialmente, ostipos de dados utilizados na simulacao.

type Doc = Stringtype Linha = Stringtype Palavra = StringfazIndice :: Doc -> [ ([Int], Palavra) ]

Neste caso, o texto e uma string como a seguinte:

doc :: Docdoc = "Imagine there’s no heaven’\n’It’s easy if you try’\n’No hell

below us’\n’Above us only sky’\n’Imagine all the people’\n’livingfor today"

Vamos utilizar este trecho e mostrar como ele vai ser transformado com a aplicacao dasfuncoes que serao definidas para realizar operacoes sobre ele:

• Dividir doc em linhas, ficando assim:[”Imagine there´s no heaven”,”It´s easy if you try”,”No hell below us”,”Above us only sky”,”Imagine all the people”,”living for today”]Esta operacao e realizada por divTudo :: Doc − > [Linha]

• Agora devemos emparelhar cada linha com seu numero de linha:[(1, ”Imagine there´s no heaven”),(2, ”It´s easy if you try”),(3, ”No hell below us”),(4, ”Above us only sky”),(5, ”Imagine all the people”),(6, ”Living for today”)]Esta operacao e realizada por numLinhas :: [Linha] − > [(Int, Linha)]

• Temos agora que dividir as linhas em palavras, associando cada palavra com o numero dalinha onde ela ocorre[(1, ”Imagine”), (1, ”there´s”), (1, ”no”), (1, ”heaven”),(2, ”It´s”), (2, ”easy”), (2, ”if”), (2, ”you”), (2, ”try”),(3, ”No”), (3, ”hell”), (3, ”below”), (3, ”us”),(4, ”Above”), (4, ”us”), (4, ”only”), (4, ”sky”),(5, ”Imagine”), (5, ”all”), (5, ”the”), (5, ”people”),(6, ”Living”), (6, ”for”), (6, ”today”)]

Esta operacao e realizada por todosNumPal :: [(Int, Linha)] − > [(Int, Palavra)]

100

Page 109: LF Apostila

• Agora e necessario ordenar esta lista em ordem alfabetica das palavras e, se a mesmapalavra ocorre em mais de uma linha, ordenar por linha.[(4, ”Above”), (5, ”all”), (3, ”below”), (2, ”easy”), (6, ”for”), (1, ”heaven”), (3, ”hell”),(2, ”It´s”), (5, ”Imagine”), (2, ”if”), (6, ”Living”), (3, ”No”), (1, ”no”),(4, ”only”), (5, ”people”), (4, ”sky”), (5, ”the”), (1, ”there´s”), (6, ”today”),(2, ”try”), (3, ”us”), (4, ”us”), (2, ”you”)]

Esta operacao e realizada por ordenaLista :: [(Int, Palavra)] − > [(Int, Palavra)]

• Agora temos que modificar a lista de forma que cada palavra seja emparelhada com a listaunitaria das linhas onde ela ocorre:[([4], ”Above”), ([5], ”all”), ([3], ”below”), ([2], ”easy”), ([6], ”for”), ([1], ”heaven”),([3], ”hell”), ([2], ”It´s”), ([5], ”Imagine”), ([2], ”if”), ([6], ”Living”), ([3], ”No”),([1], ”no”), ([4], ”only”), ([5], ”people”), ([4], ”sky”), ([5], ”the”), ([1], ”there´s”),([6], ”today”), ([2], ”try”), ([3], ”us”), ([4], ”us”), ([2], ”you”)]

Esta operacao e realizada por fazListas :: [(Int, Palavra)] − > [([Int], Palavra)]

• Agora devemos juntar as linhas que contem uma mesma palavra em uma mesma lista delinhas[([4], ”Above”), ([5], ”all”), ([3], ”below”), ([2], ”easy”), ([6], ”for”), ([1], ”heaven”),([3], ”hell”), ([2], ”It´s”), ([5], ”Imagine”), ([2], ”if”), ([6], ”Living”), ([3], ”No”),([1], ”no”), ([4], ”only”), ([5], ”people”), ([4], ”sky”), ([5], ”the”), ([1], ”there´s”),([6], ”today”), ([2], ”try”), ([3,4], ”us”), ([2], ”you”)]

Esta operacao e realizada por mistura :: [([Int], Palavra)] − > [([Int], Palavra)]

• Vamos agora diminuir a lista, removendo todas as entradas para palavras com menos de4 letras[([4], ”Above”), ([3], ”below”), ([2], ”easy”), ([1], ”heaven”), ([3], ”hell”),([2], ”It´s”), ([5], ”Imagine”), ([6], ”Living”),([4], ”only”), ([5], ”people”), ([1], ”there´s”), ([6], ”today”)]

Esta operacao e realizada por diminui :: [([Int], Palavra)] − > [([Int], Palavra)]

Usando composicao avancada, para ficar mais claro o exemplo, temos:

fazIndice = divideTudo >.> -- Doc -> [Linha]numDeLinhas >.> -- [Linha] -> [(Int, Linha)]todosNumPal >.> -- [(Int, Linha)] -> [(Int, Palavra)]ordenaLista >.> -- [(Int, Palavra)] -> [(Int, Palavra)]fazListas >.> -- [(Int, Palavra)] -> [([Int], Palavra)]mistura >.> -- [([Int], Palavra)] -> [([Int], Palavra)]diminui -- [([Int], Palavra)] -> [([Int], Palavra)]

=======================================================================divideTudo :: Doc -> [Linha]divideTudo = tiraEspaco >.> pegaLinhas

tiraEspaco :: Doc -> DoctiraEspaco [ ] = [ ]tiraEspaco (a : x)|a == ’’ = tiraEspaco x|otherwise = (a : x)

pegaLinha :: Doc -> [Linha]pegaLinha [ ] = [ ]

101

Page 110: LF Apostila

pegaLinha (a : x)|a /= ’\n’ = a : pegaLinha x|otherwise = pegaLinha x

=======================================================================numDeLinhas :: [Linha] -> [(Int, Linha)]numDeLinhas lin = zip [1 .. length lin] lin=======================================================================

Vamos considerar, inicialmente, apenas uma linha:

numDePalavras :: (Int, Linha) -> [(Int, Palavra)]numDePalavras (num, linha) = map poeNumLinha (divideEmPalavras linha)

where poeNumLinha pal = (num, pal)

divideEmPalavra :: String -> [Palavra]divideEmPalavra st = divide (tiraEspaco st)

divide :: String -> [Palavra]divide [ ] = [ ]divide st = (pegaPalavra st) : divide (tiraEspaco (tiraPalavra st))

tiraPalavra :: String -> StringtiraPalavra [ ] = [ ]tiraPalavra (a : x)|elem a espacoEmBranco = (a : x)|otherwise = tiraPalavra x

espacoEmBranco = [’\n’, ’\t’, ’ ’]

todosNumPal :: [(Int, Linha)] -> [(Int, Palavra)]todosNumPal = concat . map numDePalavras=======================================================================

Agora vamos definir a funcao de ordenacao usando quicksort:

compara :: (Int, Palavra) -> (Int, Palavra) -> Boolcompara (n1, w1) (n2, w2) = w1 < w2 || (w1 == w2 && n1 $<$ n2)

ordenaLista :: [(Int, Palavra)] -> [(Int, Palavra)]ordenaLista [ ] = [ ]ordenaLista (a : x) = ordenaLista menores ++ [a] ++ ordenaLista maiores

where menores = [b | b <- x, compara b a]maiores = [b | b <- x, compara a b]

=======================================================================fazListas :: [(Int, Palavra)] -> [([Int], Palavra)]fazListas = map mklist where mklist (n, st) = ([n], st)=======================================================================mistura :: [([Int], Palavra)] -> [([Int], Palavra)]mistura [ ] = [ ]mistura [a] = [a]mistura ((l1, w1) : (l2, w2) : rest)

102

Page 111: LF Apostila

|w1 /= w2 = (l1, w1) : mistura ((l2, w2) : rest)|otherwise = mistura ((l1 ++ l2, w1) : rest)=======================================================================diminui = filter tamanho

where tamanho (n1, pal) = length pal > 4

Este exemplo visa mostrar que as linguagens funcionais nao foram projetadas apenas parafazer calculos matematicos, como fibonacci e fatorial. Na realidade, Haskell serve tambempara estes calculos, mas sua area de aplicacao e muito mais ampla do que e imaginada poralguns programadores, defensores intransigentes de outros paradigmas de programacao. O leitordeve reler o Capıtulo introdutorio deste estudo e consultar as referencias [37, 15] para maioresinformacoes sobre este tema ou consultar o site oficial de Haskell.

4.9 Melhorando o desempenho de uma implementacao

Ate agora mostramos ao leitor formas de definir funcoes em Haskell sem nos preocupar com odesempenho de cada implementacao. Nosso objetivo tem sido apenas construir definicoes, deforma que elas funcionem. No entanto, este foi um objetivo inicial, uma vez que a preocupacaocom desempenho deve ser uma constante em qualquer programador. Nesta secao, vamos fazeralgumas observacoes relacionadas ao desempenho de funcoes, dando os primeiros passos nestadirecao. Vamos iniciar com a analise do desempenho da funcao reverse, muito utilizada nasdefinicoes de funcoes sobre listas.

4.9.1 O desempenho da funcao reverse

A funcao reverse inverte a ordem dos elementos de uma lista de qualquer tipo. Ela e pre-definidaem Haskell, e poderia ser facilmente definida por

reverse :: [t] -> [t]reverse [ ] = [ ]reverse (a : x) = reverse x ++ [a]

A despeito da simplicidade desta definicao, ela padece de um serio problema de desempenho.Vamos verificar quantos passos seriam necessarios para inverter uma lista l de n elementos,usando este definicao. A funcao chama a si mesma n vezes e, em cada uma destas chamadas,chama ++, tail e head. As funcoes head e tail sao primitivas, ou seja, exigem tempo constantepara suas execucoes. A operacao ++ deve saltar para o final de seu primeiro argumento paraconcatena-lo com o segundo. O tempo total de reverse e dado pela soma

(n − 1) + (n − 2) + (n − 3) + . . . + 1 + 0 =n−1∑i=0

i =n(n − 1)

2⇒ O(n2)

Assim, reverse precisa de um tempo proporcional ao quadrado do tamanho da lista a serinvertida. Sera que existe outra forma de implementar reverse com um desempenho melhor?A resposta e sim, ou seja, e possıvel implementa-la em um tempo proporcional ao tamanho dalista. Vejamos como isto pode ser feito.

Imagine inverter a ordem de uma pilha de livros. A ideia e retirar o livro que se encontra notopo da pilha e coloca-lo ao lado, iniciando uma nova pilha de livros. Em seguida prosseguimoscom o processo de retirar mais um livro do topo da pilha anterior e coloca-lo no topo da nova

103

Page 112: LF Apostila

pilha, ate que a pilha anterior fique vazia. Neste ponto, a nova pilha sera a pilha anterior emordem inversa.

Vamos aplicar este mesmo raciocınio na implementacao da funcao reverse. Para isto usa-remos duas listas, pl e sl, para simular as pilhas de livros. Vamos definir uma funcao auxiliar,revaux que, aplicada a duas listas, retira o elemento da cabeca da primeira, pl, e o coloca comocabeca da segunda, sl. A funcao termina quando pl ficar vazia, e o resultado sera a lista sl.Formalmente esta definicao e

revaux :: [t] -> [t] -> [t]revaux pl sl

|pl == [ ] = sl|otherwise = revaux (tail pl) ((head pl) : sl)

reverse :: [t] -> [t]reverse x = revaux x [ ]

Uma analise da complexidade desta definicao nos informa que ela e proporcional ao tamanhoda lista, ou seja, O(n) [24, 30, 7]. Esta e a forma como a funcao e implementada em Haskell.

4.9.2 O desempenho do quicsort

Pode-se questionar o desempenho desta implementacao do quicsort. Uma analise acurada desua eficiencia e impossıvel ser feita porque os tamanhos das sub-listas menores e maiores naopodem ser pre-determinados. No entanto, verifica-se que o pior caso para esta funcao acontecequando uma das sub-listas menores ou maiores contiver todos os elementos da lista original(menos o pivot) e a outra for vazia. Isto acontece quando a lista original estiver completamenteordenada. Neste caso, a analise de tempo se torna

TqsortWC(n) = n2

2 + 5n2 − 1

sendo n o tamanho da lista [30]. Isto significa que a complexidade de tempo para o pior caso eO(n2). Esta complexidade e pior que a grande maioria dos algoritmos de ordenacao, apesar deser um caso pouco provavel de acontecer. Um caso mais realista consiste na hipotese de que alista original seja dividida em duas sub-listas de tamanhos proximos. Neste caso, a analise daeficiencia de tempo medio se torna

TqsortAC(n) ≈ n log n + 2n − 1

Neste hipotese, a complexidade do quicksort e O(n log n), bem melhor, que a complexidadepara o pior caso.

A analise da eficiencia de espaco acumulado nos leva a resultados similares, ou seja

SqsortWC(n) = n2

2 + n2 e

SqsortAC= n log n + n

Isto mostra que a complexidade de espaco do quicksort para o caso medio tambem eO(n log n).

Para finalizar este Capıtulo, vamos colocar uma lista de exercıcios sobre listas. Estesexercıcios devem ser resolvidos pelo leitor para adquirir domınio sobre a construcao de listaspara as diversas finalidades.

Exercıcios.

1. Defina, em Haskell, uma funcao f que, dadas uma lista i de inteiros e uma lista l qualquer,retorne uma nova lista constituıda pela lista l seguida de seus elementos que tem posicao

104

Page 113: LF Apostila

indicada na lista i, conforme o exemplo a seguir:f [2,1,4] [’a’, ’b’, ’c’, ’d’] = [’a’, ’b’, ’c’, ’d’, ’d’, ’a’, ’b’].

2. Usando compreensao, defina uma funcao em Haskell, que gere todas as tuplas ordenadasde numeros x, y, e z menores ou iguais a um dado numero n, tal que x2 + y2 = z2.

3. Defina, em Haskell, uma funcao que calcule o Determinante de uma matriz quadrada deordem n.

4. Encontre todas as solucoes possıveis para se colocar 8 rainhas em um tabuleiro de xadrezde forma que nenhuma delas ataque qualquer uma outra.

5. Defina, em Haskell, uma funcao f que, dada uma lista l construa duas outras listas l1 e l2,de forma que l1 contenha os elementos de l de posicao ımpar e l2 contenha os elementosde l de posicao par, preservando a posicao dos elementos, conforme os exemplos a seguir:f [a, b, c, d] = [[a, c], [b, d]]f [a, b, c, d, e] = [[a, c, e], [b, d]].

6. Um pequeno visor de cristal lıquido (LCD) contem uma matriz 5x3 que pode mostrar umnumero, como 9 e 5, por exemplo:

*** **** * **** **** ** ***

O formato de cada numero e difinido por uma lista de inteiros que indicam quantos * serepetem, seguidos de quantos brancos se repetem, ate o final da matriz 5x3, comecandoda primeira linha ate a ultima:

nove, cinco, um, dois, tres, quatro, seis, sete oito, zero :: [Int]nove = [4,1,4,2,1,2,1]cinco = [4,2,3,2,4]um = [0,2,1,2,1,2,1,2,1,2,1]dois = [3,2,5,2,3]tres = [3,2,4,2,4]quatro = [1,1,2,1,4,2,1,2,1]seis = [4,2,4,1,4]sete = [3,2,1,2,1,2,1,2,1]oito = [4,1,5,1,4]zero = [4,1,2,1,2,1,4]

indicando que o numero nove e composto por 4 *s (tres na primeira linha e um na segunda),seguida de 1 espaco, mais 4 *s, 2 espacos, 1 *, 2 espacos e 1 *. Construa funcoes para:

a. Dado o formato do numero (lista de inteiros) gerar a String correspondente de * eespacos.

toString :: [Int] -> StringtoString nove ==> ‘‘**** **** * *’’

b. Faca uma funcao que transforma a String de *s e espacos em uma lista de Strings,cada uma representando uma linha do LCD:

105

Page 114: LF Apostila

type Linha = StringtoLinhas :: String -> [Linha]toLinhas ‘‘**** **** * *’’

==> [‘‘***’’, ‘‘* *’’,‘‘***’’, ‘‘ *’’, ‘‘ *’’]

c. Faca uma funcao que pegue uma lista de Strings e a transforme em uma unicaString com “n” entre cada uma delas:

showLinhas :: [Linha] -> StringshowLinhas [‘‘***’’, ‘‘* *’’, ‘‘***’’, ‘‘ *’’, ‘‘ *’’]

==> ‘‘***\n* *\n***\n *\n *’’

d. Faca uma funcao que pegue duas listas de linhas e transforme-as em uma unica listade linhas, onde as linhas originais se tornam uma unica, com um espaco entre elas:

juntaLinhas :: [LInha] -> [Linha] -> [Linha]juntaLInhas [‘‘***’’, ‘‘* *’’, ‘‘***’’, ‘‘ *’’, ‘‘ *’’]

[‘‘***’’, ‘‘* *’’, ‘‘***’’, ‘‘ *’’,‘‘ *’’]==> [‘‘*** ***’’, ‘‘* * * *’’, ‘‘*** ***’’,

‘‘ * *’’, ‘‘ * *’’]

e. Faca uma funcao que, dado um inteiro, imprima-o usando *s, espacos e “n”s. A funcao tem que funcionar para inteiros de 2 e 3 dıgitos.

tolcd :: Int -> String

Dica: use as funcoes div e mod de inteiros, a funcao (!! e a lista numeros, dada aseguir:

numeros :: [[Int]]numeros= [zero, um, dois, tres, quatro, cinco, seis, sete, oito, nove]

f. Faca uma funcao que, dada uma String de *s e espacos, retorne a representacao daString como [Int] no formato usado no LCD, ou seja, a funcao inversa de toString.

toCompact :: String -> [Int]toCompact ‘‘**** *** * *’’ = [4,1,4,2,1,2,1].

7. Defina, em Haskell, uma funcao que aplicada a uma lista l e a um inteiro n, retorne todasas sublistas de l com comprimento maior ou igual a n.

4.10 Resumo

Este Capıtulo foi dedicado inteiramente ao estudo das listas em Haskell, completando o estudodos tipos primitivos adotados em Haskell, alem do tipo estruturado produto cartesiano, represen-tado pelas tuplas. Ele se tornou necessario, dada a importancia que as listas em nas linguagensfuncionais. Foi dada enfase as funcoes pre-definidas e as Compreensoes, tambem conhecidaspor expressoes ZF, mostrando a facilidade de se construir funcoes com esta ferramenta da lin-guagem. Vimos tambem a elegancia com que algumas funcoes, como por exemplo, o quicksort,foram definidas. Finalmente foram vistas caracterısticas importantes na construcao de funcoescomo: polimorfismo, composicao, avaliacao parcial e currificacao de funcoes.

Apesar dos tipos de dados estudados ate aqui ja significarem um avanco importante, Haskellvai mais alem. A linguagem tambem permite a criacao de tipos abstratos de dados e tambemdos tipos algebricos de dados, temas a serem estudados no proximo Capıtulo.

106

Page 115: LF Apostila

Grande parte deste estudo foi baseado nos livros de Simon Thompson [35] e de Richard Bird[4]. Estas duas referencias representam o que de mais pratico existe relacionado com exercıciosusando listas. O livro de Paul Hudak [14] e tambem uma fonte de consulta importante, dada asua aplicacao a multimıdia.

Uma fonte importante de problemas que podem ser resolvidos utilizando as ferramentas aquimostradas e o livro de Steven S. Skiena e Miguel A. Revilla [33], que apresenta um catalogo dosmais variados tipos de problemas, desde um nıvel inicial ate problemas complexos e de solucaocomplexa.

Outra fonte importante de problemas matematicos envolvendo grafos e o livro de SriramPemmaraju e Steven Skiena [28]. Esta referencia implementa seus exercıcios em uma ferra-menta conhecida como Mathematica, e podem ser facilmente traduzidos para Haskell, dadaa proximidade sintatica entre os programas codificados em Haskell e as solucoes adotadas naMathematica.

107

Page 116: LF Apostila

108

Page 117: LF Apostila

Capıtulo 5

Tipos de dados complexos

”Languages which aim to improveproductivity must support modular programming well.

But new scope rules and mechanisms for separatecompilation are not enough-modularity means more than moduoles.

Our ability to decompose a problem intoparts depends directly on our ability to glue solutions together.

To assist modular programming a language must provide good glue.Functional programming languages provide two new

kinds of glue - higher - order functions and lazy evaluation.”(John Hughes in [15])

5.1 Introducao

Este Capıtulo e dedicado a construcao de novos tipos de dados, mais complexos que os ate agoraapresentados, que sao os tipos algebricos e os tipos abstratos de dados.

Vamos iniciar este estudo com as classes de tipos, conhecidas mais comumente como typeclass. O domınio deste tema e necessario porque as classes de tipos representam a forma usadaem Haskell para implementar sobrecarga de operadores. Alem disso, as classes de tipos tambemsao utilizadas na implementacao dos tipos algebricos e dos tipos abstratos de dados.

Os tipos algebricos e os tipos abstratos de dados, apesar de mais complexos, representamum aumento na expressividade e no poder de abstracao de Haskell. Por estes motivos, esteestudo deve ser de domınio pleno para quem deseja aproveitar as possibilidades que a linguagemoferece. Neste Capıtulo, tambem e feito um estudo sobre o tratamento de erros, sobre as provasde programas envolvendo tipos algebricos de dados e sobre os modulos em Haskell. O Capıtulotermina com um estudo sobre os tipos abstratos de dados e sobre lazy evaluation, utilizada nacriacao de listas potencialmente infinitas.

5.2 Classes de tipos

Ja foram vistas funcoes que atuam sobre valores de mais de um tipo. Por exemplo, a funcaolength pode ser empregada para determinar o tamanho de listas de qualquer tipo. Assim,length e uma funcao polimorfica, ou seja, com apenas uma unica definicao, ela pode ser aplicadaa uma variedade de tipos. Por outro lado, algumas funcoes podem ser aplicadas a apenas algunstipos de dados, mas nao podem ser aplicadas a todos, e tem de apresentar varias definicoes, umapara cada tipo. Sao os casos das funcoes +, -, /, etc. Estas funcoes sao sobrecarregadas.

109

Page 118: LF Apostila

Existe uma discussao antiga entre os pesquisadores sobre as vantagens e desvantagens de secolocar sobrecarga em uma linguagem de programacao. Alguns argumentam que a sobrecarganao aumenta o poder de expressividade da linguagem, uma vez que as definicoes sao distintas,apesar de terem o mesmo nome. Neste caso, elas poderiam ter nomes distintos para cadadefinicao. Outros admitem a sobrecarga como uma necessidade das linguagens, uma vez que elapermite reusabilidade e legibilidade como vantagens [35]. Por exemplo, seria tedioso usar umsımbolo para a soma de inteiros e outro para a soma de numeros reais. Esta mesma situacao severifica nos operadores de subtracao e multiplicacao. A verdade e que todas as linguagens deprogramacao admitem alguma forma de sobrecarga. Haskell nao e uma excecao e admite queseus operadores aritmeticos pre-definidos sejam sobrecarregados.

Os tipos sobre os quais uma funcao sobrecarregada pode atuar formam uma colecao de tiposchamada classe de tipos (ou type class) em Haskell. Um tipo que pertence a uma classe e ditoser uma instancia desta classe. Entre as classes de Haskell pode existir uma forma de heranca,como nas linguagens orientadas a objeto.

5.2.1 Fundamentacao das classes

A funcao elem, aplicada a um elemento e uma lista de valores do tipo deste mesmo elemento,verifica se este elemento pertence, ou nao, a lista, retornando um valor booleano. Sua definicaoe a seguinte:

elem :: t -> [t] -> Boolelem x [ ] = Falseelem x (a : y) = x == a || elem x y

Analisando a definicao da funcao aplicada a lista nao vazia, verificamos que e feito um testepara verificar se o elemento x e igual a cabeca da lista (x==a). Para este teste e utilizada afuncao de igualdade (==). Isto implica que a funcao elem so pode ser aplicada a tipos cujosvalores possam ser comparados pela funcao ==. Os tipos que tem esta propriedade formamuma classe que, em Haskell, e denotada por Eq.

Em Haskell, existem dois operadores de igualdade: == e /=. Para valores booleanos, elessao definidos da seguinte forma:

(/=), (==) :: Bool -> Bool -> Boolx == y = (x and y) or (not x and not y)x /= y = not (x == y)

E importante observar a diferenca entre == e =. O sımbolo == e usado para denotar umteste computacional para a igualdade, enquanto o sımbolo = e usado nas definicoes e no sentidomatematico normal. Na Matematica, a assertiva double = square e uma afirmacao falsa e aassertiva ⊥ = ⊥ e verdadeira, uma vez que qualquer coisa e igual a si propria. No entanto, asfuncoes nao podem ser testadas quanto a sua igualdade e o resultado da da avaliacao ⊥ == ⊥ etambem ⊥ e nao True. Isto nao quer dizer que o avaliador seja uma maquina nao matematicae sim, que seu comportamento e descrito por um conjunto limitado de regras matematicas,escolhidas de forma que elas possam ser executadas mecanicamente [4].

O objetivo principal de se introduzir um teste de igualdade e ser capaz de usa-lo em umavariedade de tipos distintos, nao apenas no tipo Bool. Em outras palavras, == e /= saooperadores sobrecarregados. Estas operacoes serao definidas de forma diferente para cada tipoe a forma adequada de introduzı-las e declarar uma classe de todos os tipos para os quais ==e /= vao ser definidas. Esta classe e pre-definida em Haskell e e denominada Eq.

110

Page 119: LF Apostila

A forma de declarar Eq como a classe dos tipos que tem os operadores ==e /= e a seguinte:

class Eq t where(==), (/=) :: t -> t -> Bool

Esta declaracao estabelece que a classe de tipos Eq contem duas funcoes membros, oumetodos, == e /=. Estas funcoes tem o seguinte tipo:

(==), (/=) :: Eq t => t -> t -> Bool

Agora o leitor pode entender o resultado mostrado pelo sistema Haskell quando detectaalgum erro de tipo na declaracao de funcoes. Normalmente, o sistema se refere a alguma classede tipos nestes casos.

5.2.2 Funcoes que usam igualdade

A funcao

todosIguais :: Int -> Int -> Int -> BooltodosIguais m n p = (m == n) && (n == p)

verifica se tres valores inteiros sao iguais, ou nao. No entanto, em sua definicao, nao e feitaqualquer restricao que a obrigue a ser definida somente para valores inteiros. A unica exigenciafeita aos elementos m, n e p e que eles possam ser comparados atraves da funcao de igualdade==. Dessa forma, seu tipo pode ser um tipo t, desde que seus elementos possem ser comparadospela funcao ==. Isto da a funcao todosIguais um tipo mais geral da seguinte forma:

todosIguais :: Eq t => t -> t -> t -> Bool

significando que ela nao e utilizada apenas sobre os tipos inteiros, mas sim, a tipos que estejamna classe Eq, ou seja, para os quais seja definida uma funcao de igualdade (==). A parte antesdo sinal => e chamada de contexto. A leitura deste novo tipo deve ser: se o tipo t esta na classeEq, ou seja, se a funcao de igualdade, == estiver definida para este tipo t, entao todosIguaistem o tipo t − > t − > t − > Bool. Isto significa que a funcao todosIguais pode ter osseguintes tipos, entre outros: Int − > Int − > Int − > Bool ou Char − > Char − > Char− > Bool ou ainda (Int, Bool) − > (Int, Bool) − > (Int, Bool) − > Bool, uma vez quetodos estes tipos tem uma funcao == definida para eles.

Vejamos agora o que acontece ao tentarmos aplicar a funcao todosIguais a argumentos dotipo funcao, como por exemplo,

suc :: Int -> Intsuc = (+1)

da seguinte forma: todosIguais suc suc suc.

O resultado mostrado pelos compiladores Haskell ou Hugs e ERROR: Int − > Int is notan instance of class Eq, significando que nao existe uma definicao da funcao == para o tipoInt − > Int (o tipo de suc).

111

Page 120: LF Apostila

5.2.3 Assinaturas e instancias

Ja foi visto que a operacao de igualdade (==) e sobrecarregada, o que permite que ela sejautilizada em uma variedade de tipos para os quais esteja definida, ou seja, para as instanciasda classe Eq. Sera mostrado agora como as classes e instancias sao declaradas. Por exemplo, aclasse Visible que transforma cada valor em uma String e da a ele um tamanho:

class Visible t wheretoString :: t -> Stringsize :: t -> Int

A definicao inclui o nome da classe (Visible) e uma assinatura, que sao as funcoes quecompoem a classe juntamente com seus tipos. Um tipo t para pertencer a classe Visible temde implementar as duas funcoes da assinatura, ou seja, coisas visıveis sao coisas que podem sertransformadas em uma String e que tenham um tamanho. Para declarar o tipo Char comouma instancia da classe Visible, devemos fazer a declaracao

instance Visible Char wheretoString ch = [ch]size _ = 1

que mostra como um caractere pode ser transformado em uma String de tamanho 1. De formasimilar, para declararmos o tipo Bool como uma instancia da classe Visible, temos de declarar

instance Visible Bool wheretoString True = "True"toString False = "False"size _ = 1

Para que o tipo Bool seja uma instancia da classe Eq devemos fazer:

instance Eq Bool whereTrue == True = TrueFalse == False = True

_ == _ = False

5.2.4 Classes derivadas

Uma classe pode herdar as propriedades de outras classes, como nas linguagens orientadas aobjetos. Como exemplo, vamos observar a classe Ord que possui as operacoes >, >=, <, <=,max, min e compare, alem de herdar a operacao == da classe Eq. Sua definicao e feita daseguinte forma:

class Eq t => Ord a where(<), (<=), (>), (>=) :: t -> t -> Boolmax, min :: t -> t -> tcompare :: t -> t -> Ordering

O tipo Ordering sera definido mais adiante, quando nos referirmos aos tipos algebricosde dados. Neste caso, a classe Ord herda as operacoes de Eq (no caso, apenas ==). Ha anecessidade de se definir pelo menos a funcao < (pode ser outra) para ser utilizada na definicaodas outras funcoes da assinatura. Estas definicoes formam o conjunto de declaracoes a seguir:

112

Page 121: LF Apostila

x <= y = (x < y || x == y)x > y = y < xx >= y = (y < x || x == y)

Vamos supor que se deseja ordenar uma lista e mostrar o resultado como uma String.Podemos declarar a funcao vSort para estas operacoes, da seguinte forma:

vSort = toString . iSort

onde toString e iSort sao funcoes ja definidas anteriormente. Para ordenar a lista e necessarioque ela seja composta de elementos que pertencam a um tipo t que possa ser ordenado, ou seja,pertenca a classe Ord. Para converter o resultado em uma String, e necessario que a lista [t]pertenca a classe Visible. Desta forma, vSort tem o tipo

vSort :: (Ord t, Visible t) => t -> String

mostrando que t deve pertencer as classes Ord e Visible. Tais tipos incluem Bool, Char,alem de outros.

Este caso de restricoes multiplas tambem pode ocorrer em uma declaracao de instancia como:

instance (Eq a, Eq b) => Eq (a, b) where(x, y) == (z, w) = x == z && y == w

mostrando que se dois tipos a e b estiverem na classe Eq entao o par (a, b) tambem esta. Asrestricoes multiplas tambem podem ocorrer na definicao de uma classe. Por exemplo,

class (Ord a, Visible a) => OrdVis a

significando que os elementos da classe OrdVis herdam as operacoes das classes Ord e Visible.

Este e um caso de declaracao de uma classe que nao contem assinatura, ou contem umaassinatura vazia. Para estar na classe OrdVis, um tipo deve semplesmente estar nas classesOrd e Visible. Neste caso, a definicao da funcao vSort anterior poderia ser modificada para

vSort :: OrdVis t => [t] -> String

O caso em que uma classe e construıda a partir de duas ou mais classes e chamado de herancamultipla.

Exercıcios

1. Como voce colocaria Bool, o tipo par (a, b) e o tipo tripla (a, b, c) como instanciasdo tipo Visible?

2. Defina uma funcao para converter um valor inteiro em uma String e mostre como Intpode ser uma instancia de Visible.

3. Qual o tipo da funcao compare x y = size x <= size y?

5.2.5 As classes pre-definidas em Haskell

Haskell contem algumas classes pre-definidas e vamos ver algumas delas com alguma explanacaosobre a sua utilizacao e definicao.

A classe Eq

Esta classe, ja descrita anteriormente, contem os tipos para os quais sao definidas funcoesde igualdade ==. Estas definicoes usam as seguintes definicoes default :

113

Page 122: LF Apostila

class Eq t where(==), (/=) :: t -> t -> Boolx /= y = not (x == y)x == y = not (x /= y)

A classe Ord

Esta e a classe que contem tipos, cujos valores podem ser ordenados. Sua definicao e:

class (Eq t) => Ord t wherecompare :: t -> t -> Ordering(<), (<=), (>=), (>) :: t -> t -> Boolmax, min :: t -> t -> t

onde o tipo Ordering tem tres resultados possıveis: LT, EQ e GT, que sao os resultadospossıveis de uma comparacao entre dois valores. A definicao de compare e:

compare x y|x == y = EQ|x <= y = LT|otherwise = GT

A vantagem de se usar a funcao compare e que muitas outras funcoes podem ser definidasem funcao dela, por exemplo,

x <= y = compare x y /= GTx < y = compare x y == LTx >= y = compare x y /= LTx > y = compare x y == GT

As funcoes max e min sao definidas por

max x y|x >= y = x|otherwise = y

min x y|x <= y = x|otherwise = y

A maioria dos tipos em Haskell pertencem as classes Eq e Ord. As excecoes sao as funcoese os tipos abstratos de dados, um tema que sera visto mais adiante, ainda neste Capıtulo.

A classe Enum

Esta e a classe dos tipos que podem ser enumerados, por exemplo, a lista [1,2,3,4,5,6] podetambem ser descrita por [1 .. 6] ou usando as funcoes da classe Enum, cuja definicao e aseguinte:

class (Ord t) => Enum a wheretoEnum :: Int -> tfromEnum :: t -> IntenumFrom :: t -> [t] -- [n ..]enumFromThen :: t -> t -> [t] -- [n, m ..]enumFromTo :: t -> t -> [t] -- [n .. m]enumFromThenTo :: t -> t -> t -> [t] -- [n, n’ .. m]

114

Page 123: LF Apostila

As funcoes fromEnum e toEnum tem as funcoes ord e chr do tipo Char como correspon-dentes, ou seja ord e chr sao definidas usando fromEnum e toEnum. Simon Thompson [35]afirma que o Haskell report estabelece que as funcoes toEnum e fromEnum nao sao significa-tivas para todas as instancias da classe Enum. Para ele, o uso destas funcoes sobre valores deponto flutuante ou inteiros de precisao completa (Integer) resulta em erro de execucao.

A classe Bounded

Esta e uma classe que apresenta um valor mınimo e um valor maximo para a classe. Suasinstancias sao Int, Char, Bool e Ordering. Sua definicao e a seguinte:

class Bounded t whereminBound, maxBound :: t

que retornam os valores mınimos e maximos de cada tipo.

A classe Show

Esta classe contem os tipos cujos valores podem ser escritos como String. A maioria dostipos pertencem a esta classe. Sua definicao e a seguinte:

type ShowS = String -> Stringclass Show a whereshowsPrec :: Int -> a -> ShowSshow :: a -> StringshowList :: [a] -> ShowS

A classe Read

Esta classe contem os tipos cujos valores podem ser lidos a partir de strings. Para usar aclasse e necessario apenas conhecer a funcao read :: Read t => String − > t. Esta classecomplementa a classe Show uma vez que as strings produzidas por show sao normalmentepossıveis de serem lidas por read.

read :: Read t => String -> t

5.3 Tipos algebricos

Ja foram vistas varias formas de modelar dados em Haskell. Vimos as funcoes de alta ordem,polimorfismo e as classes de tipos (type class) como metodologia para implementar sobrecarga.Estes dados foram modelados atraves dos seguintes tipos:

Tipos basicos (primitivos): Int, Float, Bool, Char e listas.

Tipos compostos: tuplas (t1, t2, ..., tn) e funcoes (t1− > t2), onde t1 e t2 sao tipos.

Estas facilidades que a linguagem oferece, por si so, ja mostram o grande poder de expres-sividade e de abstracao que elas proporcionam. No entanto, outros tipos de dados precisam sermodelados. Por exemplo,

• os meses: janeiro, ..., dezembro;

• tipos cujos elementos sejam numeros ou strings. Por exemplo, uma casa em uma ruapode ser identificada por um numero ou pelo nome da famılia. (nao no Brasil);

• o tipo arvore.

115

Page 124: LF Apostila

Para construir um novo tipo de dados, usamos a declaracao data que descreve como os ele-mentos deste novo tipo de dados sao construıdos. Cada elemento e nomeado por uma expressaoformulada em funcao dos construtores do tipo. Alem disso, nomes diferentes denotam elemen-tos tambem distintos. Atraves do uso de pattern matching sobre os construtores, projetam-seoperacoes que geram e processam elementos do tipo de dados escolhidos pelo programador. Ostipos assim descritos, nao as operacoes, sao chamados “tipos concretos” [35, 4] e sao modeladosem Haskell atraves dos tipos algebricos.

5.3.1 Como se define um tipo algebrico?

Um tipo algebrico e mais uma facilidade que Haskell oferece, proporcionando ao programadormais poder de abstracao, necessario para modelar algumas estruturas de dados complexas. Ou-tras possibilidades tambem existem, como por exemplo, os tipos abstratos de dados a seremvistos mais adiante. A sintaxe de uma declaracao de um tipo algebrico de dados e

data <Nome_do_tipo> = <Const1> | <Const2> | ... | <Constn>

onde Const1, Const2, ... Constn sao os construtores do tipo.

Tipos enumerados

Os tipos enumerados sao tipos algebricos utilizados para modelar a uniao disjunta de con-juntos. Como exemplo destes tipos podemos citar:

data Tempo = Frio | Quentedata Estacao = Primavera | Verao | Outono | Inverno

Neste caso, Frio e Quente sao os construtores do tipo Tempo e Primavera, Verao,Outono e Inverno sao os construtores do tipo Estacao.

As funcoes sobre estes tipos sao declaradas usando pattern matching. Para descrever atemperatura das estacoes podemos usar:

temperatura :: Estacao -> Tempotemperatura Verao = Quentetemperatura _ = Frio

Produtos de tipos

Um produto de tipos e um novo tipo de dados, cujos valores sao construıdos com mais deum construtor. Por exemplo,

data Gente = Pessoa Nome Idadetype Nome = Stringtype Idade = Int

A leitura de um valor do tipo Gente deve ser feita da seguinte forma: para construir umelemento do tipo Gente, e necessario suprir um objeto, digamos n, do tipo Nome e outro,digamos i, do tipo Idade. O elemento formado sera Pessoa n i. O construtor Pessoa funcionacomo uma funcao aplicada aos outros dois objetos, n e i. Exemplos deste tipo podem ser:

Pessoa "Constantino" 5Pessoa "Dagoberto" 2

116

Page 125: LF Apostila

Podemos formar uma funcao mostraPessoa que toma um elemento do tipo Gente e o mostrana tela:

mostraPessoa :: Gente -> StringmostraPessoa (Pessoa n a) = n ++ " -- " ++ show a

Neste caso, a aplicacao da funcao mostraPessoa (Pessoa ”John Lennon”, 60) serarespondida por

>"John Lennon -- 60"

Neste caso, o tipo tem um unico construtor, Pessoa, que tem dois elementos, Nome eIdade, para formar o tipo Gente. Nos tipos enumerados, Tempo e Estacao, os construtoressao nularios (0-arios) porque nao tem argumentos. Assim, Pessoa n a pode ser interpretadocomo sendo o resultado da aplicacao da funcao Pessoa aos argumentos n e a, ou seja, Pessoa:: Nome − > Idade − > Gente.

Uma outra definicao de tipo para Gente poderia ser

type Gente = (Nome, Idade)

Existem vantagens e desvantagens nesta nova versao, no entanto, e senso comum que ela deve serevitada. Podemos usar o mesmo nome para o tipo e para o construtor, no entanto, esta escolhapode conduzir a ambiguidades e, portanto, deve ser evitada. Por exemplo, e perfeitamente legala construcao do tipo data Pessoa = Pessoa Nome Idade.

Exemplo. Uma forma geometrica pode ser um cırculo ou um retangulo. Entao podemosmodela-la da seguinte maneira:

data Forma = Circulo Float | Retangulo Float Float

Agora podemos declarar funcoes que utilizam este tipo de dados, por exemplo, a funcao quecalcula a area de uma forma geometrica:

area :: Forma -> Floatarea (Circulo r) = pi * r * rarea (Retangulo h w) = h * w

5.3.2 A forma geral

Os exemplos motrados indicam que um tipo algebrico e declarado com a seguinte sintaxe:

data <NomedoTipo> = Con1 t11...t1k1

| Con2 t21...t2k2

| ...| Conn tn1...tnkn

onde dada Coni e um construtor seguido por ki tipos, onde ki e um numero inteiro nao negativo(pode ser zero).

Os tipos podem ser recursivos, ou seja, o tipo NomedoTipo pode ser usado como partedos tipos tij. Isto nos da listas, arvores e muitas outras estruturas de dados. Estes tipos seraovistos mais adiante neste Capıtulo.

O NomedoTipo pode ser seguido de uma ou mais variaveis de tipo que podem ser usadasno lado direito da definicao, tornando-a polimorfica.

117

Page 126: LF Apostila

5.3.3 Derivando instancias de classes

Ao se introduzir um tipo algebrico, como Estacao, podemos desejar que ele tambem tenhaigualdade, enumeracao, etc. Isto pode ser feito pelo sistema, informando que o tipo tem asmesmas funcoes que as classes Eq, Ord e Show tem:

data Estacao = Primavera | Verao | Outono | Invernoderiving (Eq, Ord, Show)

Exercıcios:

1. Redefina a funcao temperatura :: Estacao − > Tempo de forma a usar guardas emvez de pattern matching. Qual definicao deve ser preferida, em sua opiniao?

2. Defina o tipo Meses como um tipo algebrico em Haskell. Faca uma funcao que associeum mes a sua estacao. Coloque ordenacao sobre o tipo.

3. Defina uma funcao que de o tamanho do perımetro de uma forma geometrica do tipoForma.

4. Adicione um construtor extra ao tipo Forma para triangulos e estenda as funcoes area eperimetro (exercıcio anterior) para incluir os triangulos.

5. Defina uma funcao que decida quando uma forma e regular. Um cırculo e regular, umquadrado e um retangulo regular e um triangulo equilatero e regular.

5.3.4 Tipos recursivos

Exemplo. Uma expressao aritmetica simples que envolve apenas adicoes e subtracoes pode sermodelada atraves de sua BNF. Usando um tipo algebrico podemos modelar uma expressao daseguinte forma:

data Expr = Lit Int |Add Expr Expr |Sub Expr Expr

Alguns exemplos de utilizacao deste tipo de dado podem ser:

2 e modelado por Lit 22 + 3 e modelado por Add (Lit 2) (Lit 3)(3 − 1) + 3 e modelado por Add (Sub (Lit 3) (Lit 1)) (Lit 3)

Podemos criar uma funcao de avaliacao que tome como argumento uma expressao e de comoresultado o valor da expressao. Assim podemos fazer

eval :: Expr -> Inteval (Lit n) = neval (Add e1 e2) = (eval e1) + (eval e2)eval (Sub e1 e2) = (eval e1) - (eval e2)

Esta definicao e primitiva recursiva, ou seja, existe uma definicao para um caso base (Lit n)e uma definicao recursiva para os casos indutivos. Por exemplo, uma funcao que imprime umaexpressao pode ser feita da seguinte maneira:

mostraExpressao :: Expr -> StringmostraExpressao (Lit n) = show n

118

Page 127: LF Apostila

mostraExpressao (Add e1 e2)= "(" ++ mostraExpressao e1 ++ "+" ++ mostraExpressao e2 ++ ")"

mostraExpressao (Sub e1 e2)= "(" ++ mostraExpressao e1 ++ "-" ++ mostraExpressao e2 ++ ")"

Exercıcio. Construir uma funcao que calcule o numero de operadores em uma expressao.

Arvores binarias de inteiros

Uma arvore binaria ide inteiros ou e nula ou composta de nos internos e externos. Cada noou e a arvore nula ou e um numero inteiro com duas sub-arvores binarias de inteiros como seusdescendentes. Para os nos internos, as duas sub-arvores nao podem ser ambas nulas e para asfolhas elas sao nulas. Uma maneira de se modelar estas arvores em Haskell e simplesmente

data ArvoreInt = Nil | No Int ArvoreInt ArvoreInt

As arvores binarias de inteiros podem ser mostradas graficamente, conforme a Figura 5.1.

Figura 5.1: Representacoes de arvores binarias de inteiros.

Agora podemos construir funcoes para manipular estas arvores. Por exemplo, uma funcaopara somar os elementos da arvore:

somaArvInt :: ArvoreInt -> IntsomaArvInt Nil = 0somaArvInt (No n t1 t2) = n + somaArvInt t1 + somaArvInt t2

Uma funcao que retorne a profundidadede uma arvore binaria de inteiros, levando em con-sideracao que as profundidades de uma arvore Nil e da raiz de uma arvore No sao 0 (zero):

profundidade :: ArvInt -> Intprofundidade Nil = 0profundidade (No n t1 t2)|(t1 == Nil) && (t2 == Nil) = 0|otherwise = 1 + max (profundidade t1) (profundidade t2)

Ou podemos modelar uma funcao que verifique quantas vezes um numero p aparece em umaarvore do tipo ArvoreInt:

numOcorrencias :: ArvoreInt -> Int -> Int

119

Page 128: LF Apostila

numOcorrencias Nil p = 0numOcorrencias (No n t1 t2) p|n == p = 1 + (numOcorrencias t1 p) + (numOcorrencias t2 p)|otherwise = (numOcorrencias t1 p) + (numOcorrencias t2 p)

5.3.5 Recursao mutua

Uma propriedade muito difıcil de ser implementada em qualquer linguagem de programacao, sejaimperativa ou pertencente a outro paradigma, e recursao mutua. Por este motivo, a maioria daslinguagens nao permite esta facilidade. No entanto, Haskell a oferece, dando um maior poder deabstracao ao programador. Observamos, no entanto, que este tipo de recursao deve ser utilizadocom cuidado para evitar erros e/ou ambiguidades. Um exemplo pode ser

data Pessoa = Adulto Nome Endereco Biografia| Crianca Nome

data Biografia = Pai String [Pessoa] | NaoPai String

mostraPessoa (Adulto nom end bio)= mostraNome nom ++ mostraEndereco end ++ mostraBiografia bio

. . .

mostraBriografia (Pai st listaPes)= st ++ concat (map mostraPessoa listaPes)

Exercıcios:

1. Calcule:

eval (Lit 67)eval (Add (Sub (Lit 3) (Lit 1) (Lit 3))mostraExpressao (Add (Lit 67) (Lit (-34))).

2. Adicione as operacoes de divisao e multiplicacao de inteiros ao tipo Expr e re-defina asfuncoes eval, showExpr e size (que da a quantidade de operadores em uma expressao).O que sua operacao de divisao faz no caso da divisao ser por zero?

3. Calcule: somaArvInt (No 3 (No 4 Nil Nil) Nil) e profundidade (No 3(No 4 Nil Nil) Nil), passo a passo.

4. Defina uma funcao que decida se um numero inteiro e um elemento de uma ArvoreInt.

5. Defina funcoes para encontrar os valores maximo e mınimo mantidos em uma arvore deinteiros do tipo ArvoreInt.

6. Defina as funcoes transfArvLista, sort :: ArvoreInt − > [Int] que transformam umaarvore de inteiros em uma lista de inteiros. A funcao transfArvLista deve percorrer asub-arvore da esquerda, depois a raiz e, finalmente, a sub-arvore da direita (in-order). Afuncao sort deve ordenar os elementos da arvore de tal forma que os elementos da listaresultante estejam ordenados em ordem crescente. Por exemplo:

transfArvLista (No 3(No 4 Nil Nil) Nil) = [4,3]sort (No 3 (No 4 Nil Nil) Nil) = [3,4].

120

Page 129: LF Apostila

7. E possıvel estender o tipo Expr para que ele contenha expressoes condicionais IF b e1e2, onde e1 e e2 sao expressoes e b e uma expressao booleana, um membro do tipo BExp.

data Expr = Lit Int|Op Ops Expr Expr|If BExp Expr Expr

A expressao If b e1 e2 tem o valor e1 se b tiver o valor True e tem o valor e2 se b forFalse.

dataBExp = BoolLit Bool|And BExp BExp|Not BExp|Equal Expr Expr|Greater Expr Expr

Estas cinco claulas dao os seguintes valores:

• Literais booleanos: BoolLit True e BoolLit False.

• A conjuncao de duas expressoes: e True se as duas sub-expressoes argumentos tive-rem o valor True, caso contrario, o resultado sera False.

• A negacao de uma expressao: Not be tem o valor True se be for False.

• A igualdade de duas expressoes: Equal e1 e2 e True se as duas expressoes numericastiverem valores iguais.

• A ordem maior: Greater e1 e2 e True se a expressao numerica e1 tiver um valormaior que a expressao numerica e2.

A partir destes pressupostos, defina as funcoes:

eval :: Expr -> IntbEval :: BExp -> Bool

por recursao mutua e estenda a funcao show para mostrar o tipo re-definido para ex-pressoes.

5.3.6 Tipos algebricos polimorficos

As definicoes de tipos algebricos podem conter tipos variaveis como t, u, etc. Por exemplo,

data Pares t = Par t t

e podemos ter

Par 2 3 :: Pares IntPar [ ] [3] :: Pares [Int]Par [ ] [ ] :: Pares [t]

Podemos construir uma funcao que teste a igualdade das duas metades de um par:

igualPar :: Eq t => Pares t -> BooligualPar (Par x y) = (x == y)

121

Page 130: LF Apostila

As listas, ja vistas anteriormente, podem ser construıdas a partir de tipos algebricos.

data List t = Nil | Cons t (List t)deriving (Eq, Ord, Show)

Arvores binarias em geral

As arvores binarias vistas anteriormente sao arvores binarias de inteiros. Mas podemosconstruir arvores binarias onde seus nos sejam de um tipo variavel.

data Arvore t = Nil | No t (Arvore t) (Arvore t)deriving (Eq, Ord, Show)

As funcoes que manipulam arvores binarias de inteiros devem ser re-definidas para tratarcom esta nova estrutura, apesar das diferencas serem mınimas. Com relacao a profundidadedeve-se observar que esta funcao se refere a cada no e nao a uma arvore como um todo. Dito deoutra forma, cada no de uma arvore tem uma profundidade diferente, a nao ser que eles estejamno mesmo nıvel. Assim, para encontrar a profundidade de cada no em uma arvore de algum tipot, e necessario fazer uma transformacao desta arvore em uma arvore de tuplas onde o primeiroelemento e um valor do tipo t e o segundo e a profundidade do no. Se a arvore em questaofor constituıda de inteiros, Arvore Int, uma outra possibilidade e transformar esta arvore deinteiros em uma arvore de lista de inteiros. Para o caso de transformacao em uma arvore detuplas, a definicao pode ser feita da seguinte forma:

transforma :: Arvore t -> Int -> Arvore (t,Int)transforma Nil n = Niltransforma (No n t1 t2) prof = No (n, prof) (transforma t1 (prof+1))

(transforma t2 (prof+1))

arvBinInttoLista :: Arvore a -> Arvore (a, Int)arvBinInttoLista arv = transforma arv 0

Uma funcao que transforme uma arvore em uma lista pode ser construıda facilmente daseguinte forma:

transf :: Arvore t -> [t]transf Nil = [ ]transf (No x t1 t2) = transf t1 ++ [x] ++ transf t2

transf (No 12 (No 34 Nil Nil) (No 3 (No 17 Nil Nil) Nil)) = [34, 12, 17, 3]

Uma funcao que aplica uma outra funcao a todos os elementos de uma arvore transformando-a em outra arvore, fazendo o papel de mapeamento, pode ser definida como segue:

mapArvore :: (t -> u) -> Arvore t -> Arvore umapArvore f Nil = NilmapArvore f (No x t1 t2) = No (f x ) (mapTree f t1) (mapArvore f t2)

O tipo uniao

As definicoes tambem podem tomar mais de um parametro e podemos formar um tipo cujoselementos sejam de um tipo t ou de um tipo u.

data Uniao t u = Nome t | Numero u

122

Page 131: LF Apostila

Membros desta uniao sao Nome a, com a :: t, ou Numero b, com b :: u. Um tipo que sejaum nome ou um numero, pode ser dado por

Nome "Richard Nixon" :: Uniao String IntNumero 2143 :: Uniao Strig Int

Podemos agora formar uma funcao que verifique quando um elemento esta na primeira parteda uniao ou nao:

eNome :: Uniao t u -> BooleNome (Nome _) = TrueeNome (Numero _) = False

Para definir uma funcao de Uniao t u para Int (por exemplo), devemos tratar com os doiscasos:

fun :: Uniao t u -> Intfun (Nome x) = ...x...fun (Numero y) = ...y...

Por exemplo, podemos criar uma funcao que junte duas funcoes

juntaFuncoes :: (t -> v) -> (u -> v) -> Uniao t u -> vjuntaFuncoes f g (Nome x) = f xjuntaFuncoes f g (Numero y) = g y

Se tivermos uma funcao f :: t − > v e desejarmos aplica-la a um elemento do tipo Uniaot u, vai ocorrer um problema que e o de nao sabermos se o elemento pertence a primeira ou asegunda parte da uniao. Se a definicao dada para a primeira parte for aplicada a segunda parteocorrera um erro.

aplicaUm :: (t -> v) -> Uniao t u -> vaplicaUm f (Nome x) = f xaplicaUm f (Numero _) = error "aplicaUm applied to Dois"

Exercıcios:

1. Defina uma funcao twist que troca a ordem de uma uniao. twist :: Uniao t u − > Uniaou t. Qual sera o efeito da aplicacao (twist . twist)?

2. As arvores definidas anteriormente sao binarias, ou seja, cada no tem exatamente duassub-arvores. Podemos, no entanto, definir arvores mais gerais com uma lista arbitraria desub-arvores. Por exemplo,

data ArvoreGeral t = Folha t | No [ArvoreGeral t]

defina funcoes para:

(a) contar o numero de folhas em uma ArvoreGeral,

(b) encontrar a profundidade de uma ArvoreGeral,

(c) somar os elementos de uma arvore generica,

(d) verificar se um elemento esta em uma ArvoreGeral,

(e) mapear uma funcao sobre os elementos das folhas de uma ArvoreGeral e

(f) transformar uma ArvoreGeral em uma lista.

123

Page 132: LF Apostila

5.4 Tratamento de erros

Para se construir bons programas, e necessario que especificar o que o programa deve fazerno caso de acontecer algumas situacoes anomalas. Estas situacoes anomalas sao chamadas deexcecoes e, entre elas, podemos citar:

• tentativa de divisao por zero, calculo de raiz quadrada de numero negativo ou a aplicacaoda funcao fatorial a um numero negativo, entre outros,

• tentativa de encontrar a cabeca ou a cauda de uma lista vazia.

Nesta secao, este problema e analisado atraves de tres tecnicas. A solucao mais simples eexibir uma mensagem sobre o motivo da ocorrencia e parar a execucao. Isto e feito atraves deuma funcao de erro.

error :: String -> t

Uma tentativa de avaliar a expressao error ”Circulo com raio negativo.”resultaria na men-sagem

Program error: Circulo com raio negativo.

que seria impressa e a execucao do programa terminaria.

O problema com esta tecnica e que todas as informacoes usuais da computacao sao perdidas,porque o programa e abortado. Em vez disso, o erro pode ser tratado de alguma forma, semparar a execucao do programa. Isto pode ser feito atraves das duas tecnicas a seguir.

5.4.1 Valores fictıcios

A funcao tail e construıda para retornar a cauda de uma lista finita nao vazia e, se a lista forvazia, reportar esta situacao com uma mensagem de erro e parar a execucao. Ou seja,

tail :: [t] -> [t]tail (a : x) = xtail [ ] = error "cauda de lista vazia"

No entanto, esta definicao poderia ser refeita da seguinte forma:

tl :: [a] -> [a]tl (_:xs) = xstl [ ] = [ ]

Desta forma, todas as listas teriam uma resposta para uma solicitacao de sua cauda, seja elavazia ou nao. De forma similar, a funcao de divisao de dois numeros inteiros poderia ser feitada seguinte forma, envolvendo o caso onde o denominador seja zero:

divide :: Int -> Int -> Intdivide n m|(m /= 0) = n ’div’ m|otherwise = 0

124

Page 133: LF Apostila

Para estes dois casos, a escolha dos valores fictıcios e obvia. No entanto, existem casos emque esta escolha nao e possıvel. Por exemplo, na definicao da cabeca de uma lista. Neste caso,qual seria um valor fictıcio adequado? Para resolver esta situacao, temos de recorrer a umartifıcio mais elaborado. Precesamos re-definir a funcao hd, acrescentando mais um parametro.

hd :: a -> [a] -> ahd y (x:_) = xhd y [ ] = y

Esta tecnica e mais geral e consiste na definicao de uma nova funcao para o caso de ocorrererro. Neste caso, a funcao de um argumento foi modificada para ser aplicada a dois argumentos.De uma forma geral, temos de construir uma funcao com uma definicao para o caso da excecaoacontecer e outra definicao para o caso em que ela nao aconteca.

fErr y x|cond = y|otherwise = f x

Esta tecnica funciona bem em muitos casos. O unico problema e que nao e reportadanenhuma mensagem informando a ocorrencia incomum. Uma outra abordagem e processar aentrada indesejavel.

5.4.2 Tipos de erros

As tecnicas anteriores se baseiam no retorno de valores fictıcios na ocorrencia de um erro. Noentanto, uma outra abordagem e ter a possibilidade de se ter um valor erro como resultado.Isto e feito atraves do tipo Maybe.

data Maybe t = Nothing | Just tderiving (Eq, Ord, Read, Show)

Na realidade e o tipo t com um valor extra, Nothing, acrescentado. Assim podemos definira funcao de divisao errDiv da seguinte forma:

errDiv :: Int -> Int -> Maybe InterrDiv n m| (m /= 0) = Just (n ’div’ m)| otherwise = Nothing

Para o caso geral, utilizando uma funcao f, devemos fazer

fErr x| cond = Nothing| otherwise = Just (f x)

O resultado destas funcoes agora nao sao do tipo de saıda original, digamos t, mas do tipoMaybe t. Este tipo, Maybe, nos permite processar um erro. Podemos fazer duas coisas comele:

• podemos “transmitir”o erro atraves de uma funcao, que e o efeito da funcao mapMaybe,a ser vista a seguir e

125

Page 134: LF Apostila

• podemos “segurar”um erro, que e o papel da funcao maybe.

A funcao mapMaybe transmite o valor de um erro, apesar da aplicacao de uma funcaog. Suponhamos que g seja uma funcao do tipo a → b e que estejamos tentando usa-la comooperador sobre um tipo Maybe a. No caso do argumento Just x, g pode ser aplicada a x paradar o rsultado g x do tipo b. Por outro lado, se o argumento for Nothing entao Nothing e oresultado.

mapMaybe :: (a -> b) -> Maybe a -> Maybe bmapMaybe g Nothing = NothingmapMaybe g (Just x) = Just (g x)

Para amarrar um erro, deve-se retornar um resultado do tipo b, a partir de uma entrada dotipo Maybe a. Neste caso, temos duas situacoes:

• no caso Just, aplicamos a funcao de a para b e

• no caso Nothing, temos de dar o valor do tipo b que vai ser retornado.

A funcao de alta ordem que realiza este objetivo e maybe, cujos argumentos, n e f, saousados nos casos Nothing e Just, respectivamente.

maybe :: b -> (a -> b) -> Maybe a -> bmaybe n f Nothing = nmaybe n f (Just x) = f x

Podemos ver as funcoes mapMaybe e maybe em acao, nos exemplos que seguem. Noprimeiro deles, a divisao por zero nos retorna Nothing que vai sendo “empurrado”para a frentee retorna o valor 56.

maybe 56 (1+) (mapMaybe (*3) (errDiv 9 0))= maybe 56 (1+) (mapMaybe (*3) Nothing)= maybe 56 (1+) Nothing= 56

No segundo caso, uma divisao normal retorna um Just 9. Este resultado e multiplicado por3 e maybe, no nıvel externo, adiciona 1 e remove o Just.

maybe 56 (1+) (mapMaybe (*3) (errDiv 9 1))= maybe 56 (1+) (mapMaybe (*3) (Just 9))= maybe 56 (1+) (Just 27)= (1+) 27= 28

A vantagem desta tecnica e que podemos definir o sistema sem gerenciar os erros e depoisadicionar um gerenciamento de erro usando as funcoes mapMaybe e maybe juntamente comas funcoes modificadas para segurar o erro. Separar o problema em duas partes facilita a solucaode cada uma delas e do todo.

Exercıcio.

Defina uma funcao process :: [Int] − > Int − > Int − > Int de forma que process l nm toma o n-esimo e o m-esimo elementos de uma lista l e retorna a sua soma. A funcao deveretornar zero se quaisquer dos numeros n ou m nao forem ındices da lista l. Para uma lista detamanho p, os ındices sao: 0, 1, ..., p-1, inclusive.

126

Page 135: LF Apostila

5.5 Provas sobre tipos algebricos

Com os tipos algebricos tambem podemos realizar provas sobre a corretude ou nao de algunsprogramas, de forma similar as provas com outros tipos de dados. Por exemplo, reformulando adefinicao de arvore binaria, podemos ter

data Arvore t = Nil | No t (Arvore t) (Arvore t)deriving (Eq, Ord, Show)

Para provar uma propriedade P(tr) para todas as arvores finitas do tipo Arvore t, temosde analisar dois casos:

1. o caso Nil: verificar se P(Nil) e verdadeira e

2. o caso No: verificar se P(No x tr1 tr2) para todo x, assumindo P(tr1) e P(tr2).

Exemplo: provar que map f (collapse tr) = collapse (mapTree f tr).

Para provar isto devemos usar as definicoes anteriores:

map f [ ] = [ ] (1)map (a : x) = f a : map f x (2)

mapTree f Nil = Nil (3)mapTree f (No x t1 t2)

= No (f x) (mapTree f t1) (mapTree f t2) (4)

collapse Nil = [ ] (5)collapse (No x t1 t2)

= collapse t1 ++ [x] ++ collapse t2 (6)

Esquema da prova:

O caso Nil:

Lado esquerdo Lado direitomap f (collapse Nil) collapse (mapTree f Nil)= map f [ ] por (5) = collapse Nil por (3)= [ ] por (1) = [ ] por (5)

Assim, a propriedade e valida para a arvore do tipo Nil.

O caso No:

Para o caso No, temos de provar que

map f (collapse (No x tr1 tr2)) = collapse (mapTree f (No x tr1 tr2))assumindo que:

map f (collapse tr1) = collapse (mapTree f tr1) (7) emap f (collapse tr2) = collapse (mapTree f tr2) (8)

usando ainda o fato de que map g (y ++ z) = map g y ++ map g z (9)

Assim, a propriedade tambem e valida para a arvore do tipo No.

Conclusao: a propriedade e valida para os casos nil e No. Portanto, e valida para qualquerarvore binaria, ou seja,

127

Page 136: LF Apostila

Lado esquerdo Lado direitomap f (collapse(No x tr1 tr2)) collapse (mapTree f (No x tr1 tr2))=map f (collapse tr1 ++ [x] ++ = collapse (No (f x)collapse tr2) por (6) (mapTree f tr1) (mapTree f tr2)) por (4)=map f (collapse tr1) ++ [f x] ++map f (collapse tr2) por (9) =collapse (mapTree f tr1) ++ [f x]=collapse (mapTree f tr1) ++ [f x] ++ collapse(mapTree f tr2) por (6)++ collapse(mapTree f tr2) por (7 e 8)

map f (collapse tr) = collapse (mapTree f tr)

Exercıcios:

1. Usando a definicao de profundidade de uma arvore binaria, feita anteriormente, proveque, para toda arvore finita de inteiros tr, vale profundidade tr < 2(profundidade tr).

2. Mostre que para toda arvore finita de inteiros tr,

occurs tr a = length (filter (==a) (collapse tr)).

3. Prove que a funcao twist, definida anteriormente, tem a propriedade de que twist . twist= id.

5.6 Modulos em Haskell

No Capıtulo introdutorio desta Apostila, afirmamos que a principal vantagem das linguagensestruturadas era a modularidade oferecida por elas e, por este motivo, as linguagens funcionaiseram indicadas como solucao para a “crise do software” dos anos 80. De fato, as linguagensfuncionais sao modulares e Haskell oferece muitas alternativas para a construcao de modulos,obedecendo as exigencias preconizadas pela Engenharia de Software para a construcao de pro-gramas.

Para John Hughes [15], a modularidade e a caracterıstica que uma linguagem de programacaodeve apresentar para que seja utilizada com sucesso. Ele afirma que esta e a caracterıstica princi-pal das linguagens funcionais, proporcionada atraves das funcoes de alta ordem e do mecanismode avaliacao lazy.

A modularidade e importante porque:

• as partes de um sistema podem ser construıdas separadamente,

• as partes de um sistema podem ser compiladas e testadas separadamente e

• pode-se construir grandes bibliotecas para utilizacao.

No entanto alguns cuidados devem ser tomados para que estes benefıcios sejam auferidos,caso contrario, em vez de facilitar pode complicar a sua utilizacao. Entre os cuidados que devemser levados em consideracao podem ser citados:

• cada modulo deve ter um papel claramente definido,

• cada modulo deve fazer exatamente uma unica tarefa,

• cada modulo deve ser auto-contido,

• cada modulo deve exportar apenas o que e estritamente necessario e

• os modulos devem ser pequenos.

128

Page 137: LF Apostila

5.6.1 Cabecalho em Haskell

Cada modulo em Haskell constitui um arquivo. A forma de se declarar um modulo e a seguinte:

module Formiga wheredata Formigas = . . .comeFormiga x = . . .

Deve-se ter o cuidado de que o nome do arquivo deve ter a extensao .hs ou .lhs e deve ter omesmo nome do modulo. Neste caso, o arquivo deve ser Formiga.hs ou Formiga.lhs.

5.6.2 Importacao de modulos

Os modulos em Haskell podem importar dados e funcoes de outros modulos da seguinte forma:

module Abelha whereimport FormigapegadordeAbelha = . . .

As definicoes visıveis em Formiga podem ser utilizadas em Abelha.

module Vaca whereimport Abelha

Neste caso, as definicoes Formiga e comeFormiga nao sao visıveis em Vaca. Elas podemse tornar visıveis pela importacao explıcita de Formiga ou usando os controles de exportacaopara modificar o que e exportado a partir de Abelha.

5.6.3 O modulo main

Em todo sistema deve existir um modulo chamado Main que deve conter a definicao da funcaomain. Em um sistema interpretado, como Hugs, ele tem pouca importancia porque um modulosem um nome explıcito e tratado como Main.

5.6.4 Controles de exportacao

Por default, tudo o que e declarado em um modulo pode ser exportado, e apenas isto, ou seja, oque e importado de outro modulo nao pode ser exportado pelo modulo importador. Esta regrapode ser exageradamente permissiva porque pode ser que algumas funcoes auxiliares nao devamser exportadas e, por outro lado, pode ser muito restritiva porque pode existir alguma situacaoem que seja necessario exportar algumas definicoes declaradas em outros modulos.

Desta forma, deve-se poder controlar o que deve ou nao ser exportado. Em Haskell, isto edeclarado da seguinte forma: module Abelha (pegadorAbelha, Formigas(. . .), come-Formiga) where . . . ou equivalentemente module Abelha (module Abelha, moduleFormiga) where . . .

A palavra module dentro dos parenteses significa que tudo dentro do modulo e exportado,ou seja, module Abelha where e equivalente a module Abelha (module Abelha) where.

129

Page 138: LF Apostila

5.6.5 Controles de importacao

Da mesma forma que Haskell tem controles para exportacao, tem tambem controles para aimportacao.

module Tall whereimport Formiga (Formigas (. . .))

Neste caso, a intencao e importar apenas o tipo Formigas do modulo Formiga. Pode-setambem esconder alguma entidade. Por exemplo,

module Tall whereimport Formiga hiding (comeFormiga)

Nesta situacao, a intencao e esconder a funcao comeFormiga. Se em um modulo existirum objeto com o mesmo nome de outro objeto, definido em um modulo importado, pode-seacessar ambos objetos usando um nome qualificado. Como exemplo, Formiga.urso e o objetoimportado e urso e o objeto definido localmente. Um nome qualificado e construıdo a partirdo nome de um modulo e do nome do objeto neste modulo. Para usar um nome qualificado, enecessario que se faca uma importacao:

import qualified Formiga

No caso dos nomes qualificados, pode-se tambem estabelecer quais ıtens vao ser exportadose quais serao escondidos. Tambem e possıvel usar um nome local para um modulo importado,como em

import Inseto as Formiga

Nesta secao fizemos um estudo rapido sobre a utilizacao de modulos em Haskell. O objetivofoi apenas mostrar as muitas possibiolidades que a linguagem oferece. A pratica da programacaode modulos e que vai dotar o programador da experiencia e habilidade necessarias para o de-senvolvimento de programas. A proxima secao e dedicada aos tipos abstratos de dados, cujodomınio, prescinde da pratica da programacao e deve ser o metodo de estudo a ser adotado peloleitor.

5.7 Tipos abstratos de dados

O objetivo desta secao e introduzir os tipos abstratos de dados (TAD) e o mecanismo provido porHaskell para definı-los. De maneira geral, os tipos abstratos de dados diferem dos introduzidospor uma declaracao data, no sentido de que os TADs podem escolher a representacao de seusvalores. Cada escolha de representacao conduz a uma implementacao diferente do tipo de dado[35].

Os tipos abstratos sao definidos de forma diferente da forma utilizada para definir os tiposalgebricos. Um tipo abstrato nao e definido pela nomeacao de seus valores, mas pela nomeacao desuas operacoes. Isto significa que a representacao dos valores dos tipos abstratos nao e conhecida.O que e realmente de conhecimento publico e o conjunto de funcoes para minipular o tipo. Porexemplo, Float e um tipo abstrato em Haskell. Para ele, sao exibidas operacoes de comparacaoe aritmeticas alem de uma forma de exibicao de seus valores, mas nao se estabelece como taisnumeros sao representados pelo sistema de avaliacao de Haskell, proibindo o uso de casamento

130

Page 139: LF Apostila

de padroes. Em geral, o programador que usa um tipo abstrato nao sabe como seus elementossao representados. Tais barreiras de abstracao sao usuais quando mais de um programadorestiverem trabalhando em um mesmo projeto ou mesmo quando um mesmo programador estivertrabalhando em um projeto nao trivial. Isto permite que a representacao seja trocada sem afetara validade dos scripts que usam o tipo abstrato. Vamos mostrar estes fundamentos atraves deexemplos.

5.7.1 O tipo abstrato Pilha

Vamos dar inıcio ao nosso estudo dos tipos abstratos de dados atraves da implementacao dotipo Pilha. As pilhas sao estruturas de dados homogeneas onde os valores sao colocados e/ouretirados utilizando uma estrategia LIFO (Last In First Out). Informalmente, esta estruturade dados e comparada a uma pilha de pratos na qual so se retira um prato por vez e sempre oque esta no topo da pilha. Tambem so se coloca um prato por vez e em cima do prato que seencontra no topo.

As operacoes necessarias para o funcionamento de uma pilha do tipo Stack t1 sao as se-guintes:

push :: t -> Stack t -> Stack t --coloca um item no topo da pilhapop :: Stack t -> Stack t --retira um item do topo da pilhatop :: Stack t -> t --pega-se o item do topo da pilhastackEmpty :: Stack t -> Bool --verifica se a pilha eh vazianewStack :: Stack t --cria uma pilha vazia

Primeira implementacao para o TAD Pilha

Para se implementar um tipo abstrato de dados em Haskell, deve-se criar um modulo para isto.A criacao de modulos foi um estudo feito na secao anterior. O leitor deve voltar a este tema setiver alguma dificuldade de entender as declaracoes aqui feitas. Serao feitas duas implementacoesdo tipo pilha. A primeira baseada em um tipo algebrico e a segunda baseada em lista.

module Stack(Stack, push, pop, top, stackEmpty, newStack} where

push :: t -> Stack t -> Stack tpop :: Stack t -> Stack ttop :: Stack t -> tstackEmpty :: Stack t -> BoolnewStack :: Stack t

data Stack t = EmptyStk| Stk t (Stack t)

push x s = Stk x s

pop EmptyStk = error ‘‘pop em pilha vazia’’pop (Stk _ s) = x

top EmptyStk = error ‘‘topo de pilha vazia’’

1Apesar do autor dar preferencia a nomes em Portugues, nestes Exemplos, eles serao descritos com os nomescom os quais foram implementados, uma vez que muitas implementacoes ja estao incorporadas ao sistema comestes nomes e a adocao de outros nomes pode provocar confusao.

131

Page 140: LF Apostila

top (Stk x _) = x

newStack = EmptyStk

stackEmpty EmptyStk = TruestackEmpty _ = False

instance (Show t) => Show (Stack t) whereshow (emptyStk) = ‘‘#’’show (Stk x s) = (show x) ++ ‘‘|’’ ++ (show s)

A utilizacao do tipo abstrato Stack deve ser feita em outros modulos cujas funcoes necessitamdeste tipo de dado. Desta forma, o modulo Stack deve constar da relacao de modulos importadospor este modulo usuario. Por exemplo,

module Main where

import Stack

listTOstack :: [t] -> Stack tlistTOstack [ ] = new StacklistTOstack (x : xs) = push x (listTOstack xs)

stackTOlist :: Stack t -> [t]stackTOlist s

|stackEmpty s = [ ]|otherwise = (top s) : (stackTOlist (pop s))

ex1 = push 14 (push 9 (push 18 (push 26 newStack)))ex2 = push ‘‘Dunga’’ (push ‘‘Constantino’’)

Estes script deve ser salvo em um arquivo que deve ser carregado para ser utilizado. Aimplementacao mostrada foi baseada no tipo algebrico Stack t. O implementador pode encon-trar uma forma alternativa de implementacao que seja mais eficiente que esta e promover estamudanca sem que os usuarios saibam disto. O que nao pode ser modificada e a Interface como usuario, para que os programas que ja utilizam o tipo com a implementacao antiga possamcontinuar funcionando com a nova implementacao, sem qualquer modificacao no programa.

Segunda implementacao para o TAD pilha

Vamos agora mostrar uma segunda implementacao do tipo abstrato pilha baseada em listas. Asoperacoes serao as mesmas, para facilitar o entendimento e a comparacao com a implementacaoanterior.

module Stack(Stack, push, pop, top, stackEmpty, newStack} where

push :: t -> Stack t -> Stack tpop :: Stack t -> Stack ttop :: Stack t -> tstackEmpty :: Stack t -> BoolnewStack :: Stack t

132

Page 141: LF Apostila

data Stack t = Stk [t]

push x (Stk xs) = Stk (x : xs)

pop (Stk [ ]) = error ‘‘pop em pilha vazia’’pop (Stk (_ : xs)) = Stk xs

top (Stk [ ]) = error ‘‘topo de pilha vazia’’top (Stk (x : _)) = x

newStack = Stk [ ]

stackEmpty (Stk [ ]) = TruestackEmpty _ = False

instance (Show t) => Show (Stack t) whereshow (Stk [ ]) = ‘‘#’’show (Stk (x : xs)) = (show x) ++ ‘‘|’’ ++ (show (Stk xs))

O modulo Main e o mesmo para as duas implementacoes, nao sendo necessario ser mostradonovamente.

5.7.2 O tipo abstrato de dado Fila

Vamos agora mostrar um exemplo do tipo abstrato fila. A ideia basica de uma fila e que ela euma estrutura de dados do tipo FIFO (First-In First-Out), onde os elementos sao inseridosde um lado e sao retirados pelo outro (se tiver algum), imitando uma fila de espera. Pode-sedizer que uma fila e uma especie de lista finita com um conjunto restrito de operacoes. Parao programador e importante uma implementacao eficiente deste tipo Queue t, porem ele naoesta interessado como ela e feita, podendo ate solicitar a outra pessoa que o faca ou utilizar umasolucao provida pelo sistema.

Inicialmente, o implementador precisa conhecer que operacoes primitivas sao necessarias parao tipo abstrato a ser implementado, no caso, o tipo Queue t. Suponhamos que sejam:

enqueue :: t -> Queue t -> Queue t --coloca um item no fim da filadequeue :: Queue t -> Queue t --remove o item do inicio da filafront :: Queue t -> t --pega o item da frente da filaqueueEmpty :: Queue t -> Bool --testa se a fila esta vazianewQueue :: Queue t --cria uma nova fila vazia

A lista de operacoes a serem providas pelo TAD, juntamente com seus tipos, constituem a as-sinatura do TAD, no caso, Queue t. Vamos continuar o exemplo provendo duas implementacoesdo tipo abstrato.

Primeira implementacao para o TAD Queue

A primeira implementacao a ser mostrada e baseada nas listas finitas. As implementacoes dasoperacoes sao as seguintes:

module Queue (Queue, enqueue, dequeue, front, queueEmpty, newQueue) whereenqueue :: t -> Queue t -> Queue t

133

Page 142: LF Apostila

dequeue :: Queue t -> Queue tfront :: Queue t -> tqueueEmpty :: Queue t -> BoolnewQueue :: Queue t

data Queue t = Fila [t]

enqueue x (Fila q) = Fila (q ++ [x])

dequeue (Fila (x : xs)) = Fila xsdequeue _ = error ‘‘Fila de espera vazia’’

front (Fila (x : _)) = xfront _ = error ‘‘Fila de espera vazia’’

queueEmpty (Fila [ ]) = TruequeueEmpty _ = False

newQueue = (Fila [ ])

instance (Show t) => Show (Queue t) whereshow (Fila [ ]) = ‘‘.’’show (Fila (x : xs)) = ‘‘<’’ ++ (show x) ++ (show (Fila xs))

Para utilizar o TAD Queue e necessario construir o modulo Main, de forma similar a quefoi feita para a utilizacao do TAD Stack.

module Main where

import Stackimport Queue

queueTOstack :: Queue t -> Stack tqueueTOstack q = qts q newStack

where qts q s|queueEmpty q = s|otherwise = qts (dequeue q) (push (front q) s)

stackTOqueue :: Stack t -> Queue tstackTOqueue s = stq s newQueue

where stq s q|stackEmpty s = q|otherwise = stq (pop s) (enqueue (top s) q)

invQueue :: Queue t -> Queue tinvQueue q = stackTOqueue (queueTOstack q)

invStack :: Stack t -> Stack tinvStack s = queueTOstack (stackTOqueue s)

q1 = enqueue 14 (enqueue 9 (enqueue 19 newQueue))s1 = push 14 (push 9 (push 19 newStack))

134

Page 143: LF Apostila

Segunda implementacao do TAD Queue

A segunda implementacao do tipo fila leva em consideracao o desempenho da implementacao.Podemos construir um modulo, Queue, da seguinte forma:

module Queue (Queue, enqueue, dequeue, queueEmpty, newQueue) whereenqueue :: t -> Queue t -> Queue tdequeue :: Queue t -> Queue tqueueEmpty :: Queue t -> BoolnewQueue :: Queue t

data Queue t = Fila [t]

newQueue = (Fila [ ])

queueEmpty (Fila [ ]) = TruequeueEmpty _ = False

enqueue x (Fila q) = Fila (q ++ [x])

dequeue q@(Fila xs)|not (queueEmpty q) = (head q, Fila (tail q))|otherwise = error ‘‘A fila estah vazia’’

instance (Show t) => Show (Queue t) whereshow (Fila [ ]) = ‘‘.’’show (Fila (x : xs)) = ‘‘<’’ ++ (show x) ++ (show (Fila xs))

A funcao dequeue retorna um par: o ıtem removido da fila e a fila restante, caso ela naoesteja vazia. Se a fila estiver vazia, uma mensagem de erro deve ser chamada para anunciar estaexcecao.

A definicao de dequeue usa um aspecto do casamento de padroes que ainda nao foi analisadoque e o padrao q@(Fila xs). O sımbolo ”@”pode ser traduzido por ”como”, no casamento daentrada. A variavel q casa com a entrada completa, Fila xs, de forma que xs da acesso a listaa partir da qual ela esta sendo construıda. Isto significa que podemos nos referir diretamente aentrada completa e a seus componentes na definicao. Sem esta flexibilidade, a alternativa seria

dequeue (Fila xs)|not (queueEmpty (Fila xs)) = (head xs, Fila (tail xs))|otherwise = error "A fila esta vazia"

onde a fila original seria reconstruıda a partir de xs. Esta forma de declaracao implica nareplicacao de xs, de tail xs de tail (tail xs), uma vez que, nas linguagens funcionais, naoexistem atualizacoes de celulas, ou seja, todas as variaveis sao, na realidade, constantes, porqueseus valores nao podem ser modificados. Isto significa que se x for declarado como tendo o valor20, ele tera este mesmo valor ate o final da execucao. Se outro valor for necessario, uma outravariavel sera declarada contendo tal valor. Neste caso, xs tera que ser replicada porque outrosponteiros podem existir para ela. Segundo [30], este e um problema das linguagens funcionaise, na maioria dos casos, inevitavel.

O uso do padrao “@” permite algum compartilhamento, evitando o desperdıcio de memoriacom copias desnecessarias. Neste caso, a variavel q e acessada e ela e implementada como umponteiro para Fila xs, necessitando apenas celulas para q e nao para Fila xs.

135

Page 144: LF Apostila

Otimizando a implementacao

Em vez de adicionar elementos ao final da lista, podemos adiciona-los no inıcio. Esta decisaonao requer qualquer alteracao nas definicoes de newQueue e queueEmpty, mas teremos deredefinir enqueue e dequeue. Por exemplo,

enqueue x (Fila xs) = Fila (x : xs)

dequeue q@(Fila xs)|not (queueEmpty q) = (last xs, Fila (init xs))|otherwise = error "A fila esta vazia"

onde as funcoes last e init sao pre-definidas em Haskell. A funcao last retorna o ultimo elementode uma lista e init retorna a lista inicial, sem seu ultimo elemento.

Do ponto de vista da complexidade, verificamos que a funcao enqueue adicionava um ele-mento ao final da lista e agora passou a faze-lo na cabeca. Era “cara”e ficou “barata” porquetinha de percorrer toda a lista para adicionar o elemento ao seu final, ou seja, dependia dotamanho da lista. Por outro lado, com a funcao dequeue aconteceu exatamente o contrario:era “barata”e ficou “cara”, pelo mesmo motivo.

Uma otimizacao importante que se pode realizar nesta implementacao consiste em imple-mentar as filas atraves de duas listas: uma para se retirar e a outra para se adicionar elementos.A adicao de elementos e feita na cabeca da segunda lista (barata) e a remocao e feita, tambemna cabeca (tambem barata), mas da primeira lista. Este esquema funciona desta maneira ateque a primeira lista nao seja vazia. No momento em que ela se tornar vazia, mas ainde existiremelementos na segunda lista, a primeira lista passa a ser a segunda com seus elementos em ordeminversa e a segunda lista passa a ser a lista vazia. Neste caso, pode-se agora continuar o processode retirada de elementos da fila, ate que as duas listas se tornem vazias. A nova implementacaofica da seguinte forma:

data Queue a = Fila [a] [a]

emptyQ = Fila [ ] [ ]

queueEmpty (Fila [ ] [ ]) = TruequeueEmpty _ = False

enqueue x (Fila xs ys) = Fila xs (x : ys)

dequeue (Fila (x : xs) ys) = (x, Fila xs ys)dequeue (Fila [ ] ys) = dequeue (Fila (reverse ys) [ ])dequeue (Fila [ ] [ ]) = error "A fila esta vazia"

Esta implementacao e substancialmente mais eficiente que as implementacoes feitas atravesde listas unicas.

5.7.3 O tipo abstrato Set

O tipo abstrato Set e uma colecao homogenea de elementos e implementa a nocao de conjunto,de acordo com a seguinte Interface:

emptySet :: Set t --cria um conjunto vazio

136

Page 145: LF Apostila

setEmpty :: Set t -> Bool --testa se o conjunto eh vazioinSet :: (Eq t) => t -> Set t -> Bool --testa se um x estah em SaddSet :: (Eq t) => t -> Set t -> Set t --coloca um item no conjuntodelSet :: (Eq t) => t -> Set t -> Set t --remove um item de um conjuntopickSet :: Set t -> t --seleciona um item de S

E necessario testar a igualdade entre os elementos de um conjunto. Por este motivo, oselementos do conjunto tem de pertencer a classe Eq. Existem algumas implementacoes deSet que exigem restricoes adicionais sobre os elementos do conjunto. Isto significa que pode-se construir uma Interface mais rica para Set incluindo as operacoes de uniao, intersecao ediferenca de conjuntos, apesar delas poderem ser construıdas a partir das operacoes definidasnesta Interface.

module Set (Set, emptySet, setEmpty, inSet, addSet, delSet) where

emptySet :: Set tsetEmpty :: Set t -> BoolinSet :: (Eq t) => t -> Set t -> BooladdSet :: (Eq t) => t -> Set t -> Set tdelSet :: (Eq t) => t -> Set t -> Set tpickSet :: Set t -> t

data Set t = S [t] --listas sem repeticoes

emptySet = S [ ]

setEmpty (S [ ]) = TruesetEmpty _ = False

inSet _ (S [ ]) = FalseinSet x (S (y : ys)) |x == y = True

|otherwise = inSet x (S ys)

addSet x (S s) |(elem x s) = S s|otherwise = S (x : s)

delSet x (S s) = S (delete x s)

delete x [ ] = [ ]delete x (y : ys) |x == y = delete x ys

|otherwise = y : (delete x ys)

pickSet (S [ ]) = error ‘‘conjunto vazio’’pickSet (S (x : _)) = x

5.7.4 O tipo abstrato Tabela

Uma tabela, Table a b, e uma colecao de associacoes entre chaves, do tipo a, com valores dotipo b, implementando assim, uma funcao finita, com domınio em a e co-domınio b, atraves deuma determinada estrutura de dados.

O tipo abstrato Table pode ter a seguinte implementacao:

137

Page 146: LF Apostila

module Table (Table, newTable, findTable, updateTable, removeTable) wherenewTable :: Table a bfindTable :: (Ord a) => a -> Table a b -> Maybe bupdateTable :: (Ord a) => (a, b) -> Table a b -> Table a bremoveTable :: (Ord a) => a -> Table a b -> Table a b

data Table a b = Tab [(a,b)] --lista ordenada de forma crescente

newTable = Tab [ ]

findTable _ (Tab [ ]) = NothingfindTable x (Tab ((c,v) : cvs))|x < c = Nothing|x == c = Just v|x > c = findTable x (Tab cvs)

updateTable (x, z) (Tab [ ]) = Tab [(x, z)]updateTable (x, z) (Tab ((c,v) : cvs))|x < c = Tab ((x,z):(c,v):cvs)|x == c = Tab ((c,z):cvs)|x > c = let (Tab t) = updateTable (x,z) (Tab cvs)

in Tab ((c,v):t)removeTable _ (Tab [ ]) = Tab [ ]removeTable x (Tab ((c,v):cvs))|x < c = Tab ((c,v):cvs)|x == c = Tab cvs|x > c = let (Tab t) = removeTable x (Tab cvs)

in Tab ((c,v):t)

instance (Show a, Show b) => Show (Table a b) whereshow (Tab [ ]) = ‘‘ ’’show (Tab ((c,v):cvs))

= (show c)++‘‘\t’’++(show v)++‘‘\n’’++(show (Tab cvs))

Como pode ser observado, o TAD tabela foi implementado usando uma lista de pares(chave,valor) ordenada em ordem crescente pelas chaves. O modulo Main para este TAD podeser implementado da seguinte forma:

module Main where

import Table

type Numero = Integertype Nome = Stringtype Nota = Integer

pauta :: [(Numero, Nome, Nota)] -> Table Numero (Nome, Nota)pauta [ ] = newTablepauta ((x,y,z):xyzs) = updateTable (x,(y,z)) (pauta xyzs)

teste = [(1111,‘‘Dunga’’,14), (5555,‘‘Constantino’’, 15),(3333,‘‘Afonso’’,18), (2222,‘‘Cecilia’’,19), (7777,‘‘Vieira’’,14),

138

Page 147: LF Apostila

(6666,‘‘Margarida’’,26)]

Neste ponto, encerramos nosso estudo sobre os tipos abstratos de dados. Foram mostradosvarios exemplos de implementacao e esperamos ter dado uma ideia ampla da forma como estestipos podem ser implementadoe em Haskell. Na realidade os princıpios subjacentes e que daosuporte aos tipos abstratos de dados devem ser do domınio de todo programador, em qualquerparadigma de programacao, principalmente o de orientacao a objetos, ultimamente tao em moda.O leitor deve praticar a implementacao de exemplos nao mostrados mas que podem ser baseadosneles. Como sugestao, indicamos a implementacao de arvores AVL, Red-black, B, B+, B* entreoutras.

5.8 Lazy evaluation

O mecanismo de avaliacao lazy ja foi mostrado em varias oportunidades anteriores. No entanto,ele foi analisado de forma superficial e agora chegou o momento de disseca-lo com maior pro-fundidade, tentando aparelhar o leitor de ferramentas que o habilitem a tirar proveito destemecanismo engenhoso de avaliacao.

Um avaliador lazy avalia uma operacao apenas uma vez e se necessario. Isto tem influenciana Compreensao de listas e na construcao de listas potencialmente infinitas. Para entender estasinfluencias, e necessario compreender de forma plena como o avaliador funciona. Por exemplo,seja o sistema de avaliacao da funcao a seguir:

eqFunc1 a b = a + b --linha 1eqFunc1 (9 - 3) (eqFunc1 34 (9 - 3)) --linha 2

= (9 - 3) + (eqFunc1 34 (9 - 3)) --linha 3= 6 + (eqFunc1 34 6) --linha 4= 6 + (34 + 6) --linha 5= 6 + 40 --linha 6= 46 --linha 7

Deve-se observar que, nas linhas 2 e 3 deste script, a subexpressao (9 - 3) ocorre duas vezes. Nestecaso, o avaliador de Haskell avalia esta subexpressao apenas uma vez e guarda este resultadona expectativa de que ele seja novamente referenciado. Na segunda ocorrencia da mesma sub-expressao, ela ja esta avaliada. Outra caracterıstica importante e que o sistema de avaliacao sovai realizar um calculo se ele for realmente necessario. Como no λ-calculo, a ordem de avaliacaoe sempre da esquerda para a direita, ou seja, leftmost-outermost. Isto tem importancia naimplementacao: os calculos so serao realizados se realmente forem necessarios e no momento emque forem solicidatos. Veja que na avaliacao da funcao eqFunc2, a seguir, o segundo argumento(eqFunc1 34 3) nao e avaliado, uma vez que ele nao e necessario para a aplicacao da funcaoeqFunc2.

eqFunc2 a b = a + 32eqFunc2 (10 * 40) (eqFunc1 34 3)

= (10 * 40) + 32= 400 + 32= 432

Serao vistas, a seguir, algumas implicacoes que esta forma de avaliacao tem sobre a construcaode listas usando compreenssoes e sobre a construcao de listas potencialmente infinitas.

139

Page 148: LF Apostila

5.8.1 Expressoes ZF (revisadas)

Ha uma forte interdependencia entre a criacao de listas por compreensao e o sistema de avaliacaopreguicosa de Haskell. Algumas aplicacoes que usam a construcao de listas por compreensaoso podem ser completamente entendidas se conhecermos o sistema de avaliacao, principalmentequando envolver a criacao de listas potencialmente infinitas.

Uma expressao ZF tem a seguinte sintaxe:[e | q1, ..., qk], onde cada qi e um qualificador e tem uma entre duas formas:

1. um gerador: p<- lExp, onde p e um padrao e lExp e uma expressao do tipo lista ou

2. um teste: bExp, onde bExp e uma expressao booleana, tambem conhecida como guarda.

Exemplos

pares :: [t] -> [u] -> [(t,u)]pares l m = [(a,b) | a <- l, b <- m]pares [1,2,3] [4,5] = [(1,4), (1,5), (2,4), (2,5), (3,4), (3,5)]

trianRet :: Int -> [(Int, Int, Int)]trianRet n = [(a,b,c) | a <- [2..n], b <- [a+1..n], c <- [b+1..n],

a*a + b*b = c*c ]triRetan 100 = [(3,4,5), (5,12,13), (6,8,10), ..., (65,72,97)]

Deve ser observado, neste ultimo exemplo, a ordem em que as triplas foram geradas. Ele pega oprimeiro numero do primeiro gerador, em seguida o primeiro gerador do segundo, depois percorretodos os valores do terceiro gerador. No momento em que os elementos do terceiro gerador seexaurirem o mecanismo volta para o segundo gerador e pega agora o seu segundo elemento e vainovamente percorrer todo o terceiro gerador. Apos se exaurirem todos os elementos do segundogerador ele volta agora para o segundo elemento do primeiro gerador e assim prossegue ate ofinal.

Sequencia de escolhas

Para mostrar a sequencia de atividades, vamos utilizar a notacao do λ-calculo para β-reducoes. Seja e uma expressao, portanto e{f/x} e a expressao e onde as ocorrencias de xsao substituıdas por f. Formalmente, temos:

[e | v < −[a1, ..., an], q2, ..., qk]= [e{a1/v}, q2{a1/v}, ..., qk{a1/v}] ++ ... ++ [e{an/v} | q2{an/v}, ..., qk{an/v}].Por exemplo, temos:

[(a, b) | a < −l]{[2, 3]/l} = [(a, b) | a < −[2, 3]] e(a + sumx){(2, [3, 4])/(a, x)} = 2 + sum[3, 4].

Regras de teste

Na utilizacao desta sintaxe e necessario que algumas regras de aplicacao sejam utilizadas.Estas regras sao importantes para entender porque algumas aplicacoes, envolvendo a criacao delistas, nao chegam aos resultados pretendidos.

[e | True, q2, ..., qk] = [e | q2, ..., qk],[e | False, q2, ..., qk] = [][e |] = [e].

Exemplos

Vamos mostrar a sequencia de operacoes para alguns exemplos para ficar mais claro:

140

Page 149: LF Apostila

[a + b | a <- [1,2], isEven a, b<- [a..2*a]]= [1 + b | isEven 1, b<- [1..2*1]] ++ [2 + b | isEven 2, b<- [2..2*2]]= [1 + b | False, b<- [1..2*1]] ++ [2 + b | True, b<- [2..2*2]]= [ ] ++ [2 + b |, b<- [2..2*2]]= [2 + 2 |, 2 + 3 |, 2 + 4 | ]= [2 + 2, 2 + 3, 2 + 4]= [4, 5, 6]

[(a, b) |a <- [1..3], b<- [1..a]]= [(1, b) | b<- [1..1] ++ [[(2, b) | b<- [1..2]] ++ [(3, b) | b<- [1..3]]= [(1, 1) |] ++ [(2, 1) |] ++ [(2, 2)|] ++ [(3, 1) |] ++ [(3, 2) |] ++ [(3, 3) |]= [(1, 1), (2, 1), (2, 2), (3, 1), (3, 2), (3, 3)]

Exercıcio. Faca o calculo da expressao [a + b | a < −[1..4], b < −[2..4], a < b].

5.8.2 Dados sob demanda

Seja encontrar a soma das quartas potencias dos numeros 1 a n. Entao os seguintes passos seraonecessarios para resolver este problema:

• construir a lista [1..n],

• elevar a quarta potencia cada numero da lista, gerando a lista [1, 16, ..., n4] e

• encontrar a soma dos elementos desta lista: = 1 + 16 + ... + n4.

Desta forma a funcao somaQuartaPotencia pode ser definida da seguinte forma:

somaQuartaPotencia n = sum (map (^4) [1..n])= sum (map (^4)(1: [2..n]))= sum (^4)(1:map(^4) [2..n])= (1^4) + sum (map (^4) [2..n])= 1 + sum (map (^4) [2..n])= 1 + sum ((^4)(2: [3..n])= 1 + sum ((^4) 2 : map (^4) [3..n])= 1 + (16 + (81 + ... + n^4))

Deve ser observado que a lista nao e criada toda de uma so vez. Tao logo uma cabeca sejacriada, toma-se a sua quarta potencia e em seguida sera aplicada a soma com algum outro fatorque vai surgir em seguida.

5.8.3 Listas infinitas

Listas infinitas nao tem sido utilizadas na maioria das linguagens de programacao. Mesmo naslinguagens funcionais, elas so sao implementadas em linguagens que usam lazy evaluation. Narealidade, estas listas nao sao infinitas e sim sao potencialmente infinitas. Vejamos um exemplo.

uns :: [Int]uns = 1 : uns -- a lista infinita de 1’s [1, 1, ..]

somaPrimdoisUns :: [Int] -> IntsomaPrimDoisUns (a : b : x) = a + b

141

Page 150: LF Apostila

Seja agora a chamada a esta funcao com o argumento uns.

somaPrimDoisUns uns= somaPrimDoisUns (1 : uns)= somaPrimDoisUns (1 : 1 : uns)= 1 + 1 = 2.

Apesar de uns ser uma lista infinita, o mecanismo de avaliacao nao precisa calcular toda a lista,para depois somar apenas os seus primeiros dois elementos. Assim que o mecanismo encontraestes elementos, a soma e realizada e a execucao acaba. Mais exemplos, a seguir:

Exemplos.

1. A lista dos triangulos retangulos.

trigRet = [(a, b, c) | c < −[2..], b < −[2..c − 1], a < −[2..b − 1], a ∗ a + b ∗ b = c ∗ c]

Verifique o que aconteceria se a ordem dos geradores fosse invertida!

2. O crivo de Eratotenes. O crivo de Eratostenes e uma lista de inteiros criada a partir deuma lista inicial. A lista inicial pode ser uma lista qualquer de inteiros. A partir dela,o primeiro elemento desta lista fara parte da nova lista que consiste deste elemento e docrivo da lista que e feita retirando-se os multiplos deste valor. Assim, crivo e definido daseguinte forma:

crivo :: [Int] -> [Int]crivo [ ] = [ ]crivo (x : xs) = x : crivo [y | y <- xs, mod y x > 0]

3. A lista dos numeros primos. A lista infinita dos numeros primos pode ser definida a patirdo crivo de Eratostenes definido anteriormente.

primos :: [Int] -> [Int]primos = crivo [2..]

Exercıcio

Defina listas infinitas de fatorial e de Fibonacci.

5.9 Resumo

Este Capıtulo foi dedicado ao estudo de varios temas. No entanto o objetivo maior foi analisaros tipos de dados complexos, em particular, os tipos algebricos e os tipos abstratos de dados. Foivisto como eles podem ser construıdos em Haskell e foram mostrados alguns exemplos para queo leitor possa seguı-los e compreender como eles podem modelar problemas reais ou imaginarios.Na realidade, programar e simular problemas construindo modelos para estes problemas, deforma que estes modelos possam ser processados por um computador, emitindo solucoes paraos modelos e estas solucoes sao interpretadas para serem aplicadas aos problemas reais.

Para possibilitar o uso destes tipos de dados, uma gama de ferramentas foram construıdas,em Haskell. Entre elas a utilizacao de modulos, o mecanismo de avaliacao lazy, as compreenssoes,alem de outras.

Dadas as grandes possibilidades de construcao de tipos que a linguagem oferece, o objetivodo Capıtulo foi mostrar a grande gama de problemas em cujas solucoes a linguagem Haskellpode ser aplicada com sucesso.

142

Page 151: LF Apostila

A grande fonte de exemplos e exercıcios mostrados neste Capıtulo, foram os livros de SimonThompson [35] e Richard Bird [4]. No entanto, um fonte importante de problemas a seremresolvidos podem ser o livro de Steven Skiena [33] e os sites:

http://www.programming-challenges.com e

http://online-judge.uva.es.

As descricoes de arvores AVL, B, B+, B*, red-black e outros tipos podem ser encontradasna bibliografia dedicada aos temas Algoritmos e Estruturas de Dados. O leitor e aconselhado aconsultar.

Para quem deseja conhecer mais aplicacoes de programas funcionais, o livro de Paul Hudak[14] e uma excelente fonte de estudo, principalmente para quem deseja conhecer aplicacoes damultimıdia usando Haskell.

143

Page 152: LF Apostila

144

Page 153: LF Apostila

Capıtulo 6

Programacao com acoes em Haskell

”... for exemple, a computation implying the modification ofa state for keeping track of the number of evaluatin steps

might be described by a monad which takes as input parameter andreturns the new state as part of its result.

Computations raising exceptions or performing input-output canalso be described by monads.”

(Fethi Rabhi et Guy Lapalme in [30])

6.1 Introducao

Este Capıtulo e dedicado a forma utilizada por Haskell para se comunicar com o mundo exterior,ou seja, para fazer operacoes de I/O. Isto se faz necessario porque, para o paradigma funcional,os programas sao expressoes que sao avaliadas para se encontrarem valores que sao atribuıdosa nomes. Para Haskell, o resultado de um programa e o valor de nome main no Modulo Maindo arquivo Main.hs. No entanto, os valores dos nomes sao imutaveis durante a execucao doprograma, ou seja, o paradigma funcional nao admite atribuicoes destrutivas.

Mas a realidade e que a grande maioria dos programas exige alguma interacao com o mundoexterno. Por exemplo:

• um programa pode necessitar ler alguma entrada de algum terminal ou escrever neste ouem outro terminal,

• um sistema de e-mail le e escreve em arquivos ou em canais e

• um programa pode querer mostrar uma figura em uma janela do monitor.

Historicamente, as operacoes de I/O representaram um desafio muito grande durante muitotempo para os usuarios das linguagens funcionais. Algumas delas tomaram rumos distintos nasolucao destes problemas. Por exemplo, Standard ML [27] preferiu incluir operacoes como

inputInt :: Int

cujo efeito e a leitura de um valor inteiro a partir do dispositivo padrao de entrada. Estevalor lido e atribuıdo a inputInt. Mas surge um problema porque, cada vez que inputInt eavaliado, um novo valor e a ele tribuıdo. Esta e uma caracterıstica do paradigma imperativo,nao do modelo funcional. Por este motivo, diz-se que SML admite um modelo funcional impuro,porque admite atribuicoes destrutivas.

Seja a seguinte definicao de uma funcao, em SML, que calcula a diferenca entre dois inteiros:

145

Page 154: LF Apostila

inputDif = inputInt - inputInt

Suponha que o primeiro ıtem de entrada seja 10 e o segundo seja 20. Dependendo da ordemem que os argumentos de inputDif sejam avaliados, ela pode ter como resultado os valores 10ou -10. Isto corrompe o modelo, uma vez que esperava-se o valor 0 (zero) para inputDif.

A razao deste problema e que o significado de uma expressao nao e mais determinado sim-plesmente pela observacao dos significados de suas partes, porque nao podemos mais atribuirum significado a inputInt sem antes saber em que local do programa ele ocorre. A primeira ea segunda ocorrencias de inputInt em inputDif podem ocorrer em diferentes tempos e podemter diferentes valores.

Um segundo problema com esta tecnica e que os programas se tornam extremamente difıceisde serem seguidos, porque qualquer definicao em um programa pode ser afetada pela presencade operacoes de I/O.

Por causa disto, durante muito tempo, as operacoes de I/O se tornaram um desafio para aslinguagens funcionais e varias tentativas foram feitas na busca de solucoes que nao alterassem oparadigma funcional.

6.2 Entrada e Saıda em Haskell

Como ja descrito, um programa funcional consiste em uma expressao que e avaliada para en-contrar um valor que sera ligado a um identificador. No caso de uma operacao de IO, que valordeve ser retornado? Por exemplo, em uma operacao de escrita de um valor na tela do monitor,que valor deve ser retornado? Este retorno e necessario para que o paradigma seja obedecido.Caso contrario, ele e corrompido.

A solucao adotada pelos idealizadores de Haskell foi introduzir um tipo especial chamado“acao”. Quando o sistema Haskell detecta um valor deste tipo, ele sabe que uma acao deve serexecutada e nao um calculo para encontrar um valor a ser nomeado. Existem acoes primitivas,por exemplo escrever um caractere em um arquivo ou receber um caractere do teclado, mastambem acoes compostas como imprimir uma string inteira em um arquivo.

As expressoes em Haskell, cujos resultados de suas avaliacoes sejam acoes, sao chamadas de“comandos”, porque elas comandam o sistema para realizar alguma acao. As funcoes, cujosretornos sejam acoes, tambem sao chamadas de comandos. Todos os comandos realizam acoes eretornam um valor de um determinado tipo T, que pode ser usado, futuramente, pelo programa.

Haskell prove o tipo IO a para permitir que um programa faca alguma operacao de I/Oe retorne um valor do tipo a. Haskell tambem prove um tipo IO () que contem um unicoelemento, representado por (). Uma funcao do tipo IO () representa uma operacao de I/O(acao) que retorna o valor (). Semanticamente, este e o mesmo resultado de uma operacao deI/O que nao retorna qualquer valor. Por exemplo, a operacao de escrever a string “Olha euaqui!”pode ser entendida desta forma, ou seja, um objeto do tipo IO ().

Existem muitas funcoes pre-definidas em Haskell para realizar acoes, alem de um mecanismopara sequencializa-las, permitindo que alguma acao do modelo imperativo seja realizada semferir o modelo funcional.

6.2.1 Operacoes de entrada

Uma operacao de leitura de um caractere (Char), a partir do dispositivo padrao de entrada, edescrita em Haskell pela funcao pre-definida getChar do tipo:

146

Page 155: LF Apostila

getChar :: IO Char

De forma similar, para ler uma string, a partir do dispositivo padrao de entrada, usamos afuncao pre-definida getLine do tipo:

getLine :: IO String

As aplicacoes destas funcoes devem ser interpretadas como operacoes de leitura seguidas deretornosi; no primeiro caso de um caractere e, no segundo, de uma string.

6.2.2 Operacoes de saıda

A operacao de impressao de um texto, e feita por uma funcao que toma a string a ser escritacomo entrada, escreve esta string no dispositivo padrao de saıda e retorna um valor do tipo ().Esta funcao foi citada no Capıtulo 3, mas de forma generica e sem nenhuma profundidade, umavez que, seria difıcil o leitor entender sua utilizacao com os conhecimentos sobre Haskell, ateaquele ponto, adquiridos. Esta funcao e putStr, pre-definida em Haskell, com o seguinte tipo:

putStr :: String -> IO ()

Agora podemos escrever ”Olha eu aqui!”, da seguinte forma:

aloGalvao :: IO ()aloGalvao = putStr "Olha eu aqui!"

Usando putStr podemos definir uma funcao que escreva uma linha de saıda:

putStrLn :: String -> IO ()putStrLn = putStr . (++ "\n")

cujo efeito e adicionar o caractere de nova linha ao fim da entrada passada para putStr.

Para escrever valores em geral, Haskell prove a classe Show com a funcao

show :: Show a => a -> String

que e usada para transformar valores, de varios tipos, em strings para que possam ser mostradasatraves da funcao putStr. Por exemplo, pode-se definir uma funcao de impressao geral

print :: Show a => a -> IO ()print = putStrLn . show

Se o objetivo for definir uma acao de I/O que nao realize qualquer operacao de I/O, masque retorne um valor, pode-se utilizar a funcao

return :: a -> IO a

cujo efeito e nao realizar qualquer acao de I/O e retornar um valor do tipo a.

147

Page 156: LF Apostila

6.2.3 A notacao do

A notacao do e um mecanismo flexıvel, construıdo para suportar duas coisas em Haskell:

1. a sequencializacao de acoes de I/O e

2. acaptura de valores retornados por acoes de I/O, para repassa-los futuramente para outrasacoes do programa.

Por exemplo, a funcao putStrLn str, descrita anteriormente, e pre-definida em Haskell efaz parte do Prelude padrao de Hugs. Ela realiza duas acoes. a primeira e escrever a string strno dispositivo padrao de saıda e a segunda e fazer com que o prompt salte para a proxima linha.Esta mesma operacao pode ser definida utilizando-se a notacao do, da seguinte forma:

putStrLn :: String -> IO ()putStrLn str = do putStr str

putStr "\n"

Neste caso, o efeito da notacao do e a sequencializacao das acoes de I/O, em uma unicaacao. A sintaxe da notacao do e regida pela regra do offside e pode-se tomar qualquer numerode argumentos (acoes).

Como outro exemplo, pode-se querer escrever alguma coisa n vezes. Por exemplo, pode-se querer fazer 4 vezes a mesma escrita do exemplo anterior. Uma primeira versao para estaoperacao pode ser o seguinte codigo em Haskell:

faz4vezes :: String -> IO ()faz4vezes str = do putStrLn str

putStrLn strputStrLn strputStrLn str

Apesar de funcionar corretamente, esta declaracao mais parece com o metodo da ”forcabruta”. Uma forma bem mais elegante de descreve-la pode ser transforma a entrada da quanti-dade de vezes que se deseja que a acao seja realizada em um parametro.

fazNvezes :: Int -> String -> IO ()fazNvezes n str = if n <= 1 then putStrLn str

else do putStrLn strfazNvezes (n-1) str

Deve ser observada a forma de recursao na cauda utilizada na definicao da funcao fazNvezes,simulando a instrucao de controle while, tao comum nas linguagens imperativas. Agora a funcaofaz4vezes pode ser redefinida por

faz4vezes = fazNvezes 4

Apesar de terem sido mostrados apenas exemplos de saıda, as entradas tambem podem serparte de um conjunto de acoes sequencializadas. Por exemplo, pode-se querer ler duas linhas dodispositivo de entrada padrao e escrever a frase “duas linhas lidas”, ao final. Isto pode ser feitoda seguinte forma:

leia2linhas :: IO ()leia2linhas = do getLine

getLineputStrLn "duas linhas lidas"

148

Page 157: LF Apostila

Capturando os valores lidos

No ultimo exemplo mostrado, foram lidas duas linhas mas nada foi feito com o resultado dasacoes de getLine. No entanto deve ser possıvel utilizar estas linhas no restante do programa.Isto e feito atraves da nomeacao dos resultados das acoes de IO a. Por exemplo,

getNput :: IO ()getNput = do linha <- getLine

putStrLn linha

onde “linha <-”nomeia o resultado de getLine.

Apesar do identificador linha parecer com uma variavel em uma linguagem imperativa, seusignificado em Haskell e bem diferente.

6.3 Arquivos, canais e descritores

Os arquivos sao considerados como variaveis permanentes, cujos valores podem ser lidos ouatualizados em momentos futuros, depois que o programa que os criou tenha terminada a suaexecucao. E desnecessario comentar a importancia que estas variaveis tem sobre a computacaoe a necessidade de suas existencias. No entanto, e necessaria uma forma de comunicacao dousuario com os arquivos. Ja foi vista uma forma, atraves do comando do. A outra, que seravista a seguir, e atraves de descritores.

Para obter um descritor (do tipo Handle) de um arquivo e necessaria a operacao de aberturadeste arquivo para que futuras operacoes de leitura e/ou escrita possam acontecer. Alem disso, enecessaria uma operacao de fechamento deste arquivo, apos suas operacoes terem sido realizadas,para que os dados que ele deve conter, nao sejam perdidos quando o programa de usuarioterminar sua execucao. Estas operacoes sao descritas em Haskell, da seguinte forma:

data IOMode = ReadMode | WriteMode | AppendMode | ReadWriteModeopenFile :: FilePath -> IOMode -> IO HandlehClose :: Handle -> IO ()

Por convencao, todas as funcoes (normalmente chamadas de comandos) usadas para tratarcom descritores de arquivos sao iniciadas com a letra h. Por exemplo, as funcoes

hPutChar :: Handle -> Char -> IO ()hPutStr :: Handle -> String -> IO ()hPutStrLn :: Handle -> String -> IO ()hPrint :: Show a => Handle -> a -> IO ()

sao utilizadas para escrever alguma coisa em um arquivo. Ja os comandos

hGetChar :: Handle -> IO ()hGetLine :: Handle -> IO ()

sao utilizados nas operacoes de leituras em arquivos. As funcoes hPutStrLn e hPrint incluemum caractere ’\n’ para a mudanca de linha ao final da string.

Haskell tambem permite que todo o conteudo de um arquivo seja retornado como uma unicastring, atraves da funcao

hGetContents :: Handle -> String

149

Page 158: LF Apostila

No entanto, ha que se fazer uma observacao. Apesar de parecer que hGetContents retornatodo o conteudo de um arquivo de uma unica vez, nao e realmente isto o que acontece. Narealidade, a funcao retorna uma lista de caracteres, como esperado, mas de forma lazy, onde oselementos sao lidos sob demanda.

6.3.1 A necessidade dos descritores

Lembremos que um arquivo tambem pode ser escrito sem o uso de descritores. Por exemplo,pode-se escrever em um arquivo usando o comando

type FilePath = StringwriteFile :: FilePath -> String -> IO ()

Tambem podemos acrescentar uma string ao final de um arquivo com o comando

appendFile :: FilePath -> String -> IO ()

Entao, qual a necessidade de se utilizar descritores? A resposta e imediata: eficiencia. Vamosanalisar.

Toda vez que o comando writeFile ou appendFile e executado, deve acontecer tambemuma sequencia de acoes, ou seja, o arquivo deve ser inicialmente aberto, a string deve ser escritae, finalmente, o arquivo deve ser fechado. Se muitas operacoes de escrita forem necessarias,entao serao necessarias muitas destas sequencias. Ao se utilizar descritores, necessita-se apenasde uma operacao de abertura no inıcio e outra de fechamento no final.

6.3.2 Canais

Os descritores tambem podem ser associados a canais, que sao portas de comunicacao naoassociadas diretamente a um arquivo. Os canais mais comuns sao: a entrada padrao (stdin),a area de saıda padrao (stdout) e a area de erro padrao (stderr). As operacoes de IO paracaracteres e strings em canais incluem as mesmas listadas anteriormente para a manipulacao dearquivos. Na realidade, as funcoes getChar e putChar sao definidas como:

getChar = hGetChar stdinputChar = hputChar stdout

Ate mesmo hGetContents pode ser usada com canais. Neste caso, o fim de um canal esinalizado com um cartactere de fim de canal que, na maioria dos sistemas, e Ctrl-d.

6.4 Gerenciamento de excecoes

Vamos agora nos reportar a erros que podem acontecer durante as operacoes de IO. Por exemplo,pode-se tentar abrir um aquivo que ainda nao existe, ou pode-se tentar ler um caractere de umarquivo que ja atingiu seu final. Certamente, nao se deve querer que o programa pare porestes motivos. O que normalmente se espera, e que o erro seja reportado como uma “condicaoanomala”, mas que possa ser corrigida, sem a necessidade de que o programa seja abortado. Parafazer este “gerenciamento de excecoes”, em Haskell, sao necessarios apenas alguns comandos deIO.

As excecoes tem o tipo IOError. Entre as operacoes permitidas sobre este tipo, esta umacolecao de predicados que podem ser usados para testar tipos particulares de excecoes. Porexemplo,

150

Page 159: LF Apostila

isEOFError :: IOError -> Bool

detecta o fim de um arquivo.

Mesmo assim, existe uma funcao catch que faz o gerenciamento de excecoes. Seu primeiroargumento e a acao de IO que se esta tentando executar e seu segundo argumento e um “descritorde excecoes”, do tipo IOError − > IO a. Vejamos:

catch :: IO a -> (IOError -> IO a) -> IO a

Isto significa que no comando textbfcatch com ger, a acao que ocorre em com (pode ate geraruma sequencia longa de acoes, podendo ate mesmo ser infinita) sera executada pelo gerenciadorger. O controle e efetivamente transferido para o gerenciador atraves de sua aplicacao a excecaoIOError. Por exemplo, esta versao de getChar retorna um caractere de uma nova linha, sequalquer tipo de execucao for encontrada.

getChar’ :: IO ChargetChar’ = catch getChar (\e -> return ’\n’)

No entanto, ele trata todas as excecoes da mesma maneira. Se apenas a excecao de fim dearquivo deve ser reconhecida, o valor de IOError deve ser solicitado.

getChar’ :: IO ChargetChar’ = catch getChar (\e -> if isEOFError e then return ’\n’

else ioError e)

A funcao isError usada neste exemplo “empurra” a excecao para o proximo gerenciador deexcecoes. Em outras palavras, permitem-se chamadas aninhadas a catch e estas, por sua vez,produzem gerenciadores de excecoes, tambem aninhados. A funcao ioError pode ser chamadade dentro de uma sequencia de acoes normais ou a partir de um gerenciador de excecoes comoem getChar, deste exemplo.

Usando-se getChar’, pode-se redefinir getLine para demonstrar o uso de gerenciadoresaninhados.

getLine’ :: IO StringgetLine’ = catch getLine’’ (\err -> "Error: " ++ show err)

where getLine’’ = do c <- getChar’if c == ’\n’ then return ""

else do l <- getLine’return (c:l)

6.5 Resumo

Este foi o Capıtulo final deste trabalho, dedicado a semantica de acoes, adotadas em Haskell,para tratar operacoes de entrada e saıda, representando a comunicacao que o programa deveter com perifericos e com os arquivos. Na realidade, ela e implementada em Haskell atraves deMonadas, uma teoria matematica bastante complexa e que, por este motivo, esta fora do escopodeste estudo.

O objetivo do Capıtulo foi mostrar as formas como Haskell trata as entradas e as saıdas dedados, ou seja, que facilidades a linguagem Haskell oferece para a comunicacao com o mundo

151

Page 160: LF Apostila

externo ao programa. Isto inclui a leitura e escrita de dados em arquivos, bem como a criacaoe o fechamento de arquivos, armazenando dados para serem utilizados futuramente.

Este estudo foi baseado nos livros de Simon Thompson [35], de Richard Bird [4] e de PaulHudak [14]. Este ainda e considerado um tema novo pelos pesquisadores das linguagens funcio-nais e acreditamos ser este o motivo que o ele e ainda muito pouco tratado na literatura. Outrapossibilidade da ausencia de publicacoes nesta area pode ser o grau de dificuldade imposto noestudo dos Monadas, que e considerado muito alto pela grande maioria dos pesquisadores daarea.

Com este Capıtulo, esperamos ter cumprido nosso objetivo inicial que foi o de proporcionaraos estudantes iniciantes das linguagens funcionais, um pouco da fundamentacao destas lingua-gens e de suas aplicacoes, notadamente em programacao funcional usando Haskell, a linguagemfuncional mais em uso, no momento.

152

Page 161: LF Apostila

Bibliografia

[1] AHO, Alfred V; SETHI, Ravi et ULLMAN, Jeffrey D. Compilers, Principles, Techniques,and Tools. 2nd. Edition. Addison-Wesley Publishing Company; 1988.

[2] ANDRADE, Carlos Anreazza Rego. AspectH: Uma Extensao Orientada a Aspectos deHaskell. Dissertacao de Mestrado. Centro de Informatica. UFPE. Recife, Fevereiro 2005.

[3] BARENDREGT, H. P. em The Lambda Calculus: Its Syntax and Semantics. (RevisedEdn.). North Holland, 1984.

[4] BIRD, Richard. Introduction to Functional Programming Using Haskell. 2nd. Edition.Prentice Hall Series in Computer Science - Series Editors C. A. Hoare and Richard Bird.1998.

[5] BRAINERD, W. S. et LANDWEBER, L. H. Theory of Computation. John Wiley & Sons,1974.

[6] CURRY, H. B. et FEYS, R. and CRAIG, W. Combinatory Logic. Volume I; North Holland,1958.

[7] DAVIE, Antony J. T. An Introduction to Functional Programming Systems Using Haskell.Cambridge Computer Science Texts. Cambridge University Press. 1999.

[8] DE SOUZA, Francisco Vieira. Gerenciamento de Memoria em ΓCMC. Dissertacao deMestrado. CIn-UFPE. Marco de 1994.

[9] DE SOUZA, Francisco Vieira. Teoria das Categorias: A Linguagem da Computacao.Exame de Qualificacao. Centro de Informatica. UFPE. 1996.

[10] DE SOUZA, Francisco Vieira et LINS, Rafael Dueire. Aspectos do Comportamento Espaco-temporal de Programas Funcionais em Uni e Multiprocessadores. X Simposio Brasileiro deArquitetura de Computadores e Processamento de Alto Desempenho. Buzios-RJ. Setem-bro. 1998.

[11] DE SOUZA, Francisco Vieira et LINS, Rafael Dueire. Analysing Space Behaviour ofFunctional Programs. Conferencia Latino-americana de Programacao Funcional. Recife-Pe. Marco. 1999.

[12] DE SOUZA, Francisco Vieira. Aspectos de Eficiencia em Algoritmos para o GerenciamentoAutomatico Dinamico de Memoria. Tese de Doutorado. Centro de Informatica-UFPE.Recife. Novembro. 2000.

[13] HINDLAY, J. Roger. Basic Simple Type Theory. Cambridge Tracts in Theorical ComputerScience, 42. Cambridge University Press. 1997.

[14] HUDAK, Paul. The Haskell School of Expression: Learning Funciotonal ProgrammingThrough Multimedia. Cambridge University Press, 2000.

153

Page 162: LF Apostila

[15] HUGHES, John. Why Functional Programming Matters. In Turner D. T. Ed. ResearchTopics in Funcitonal Programming. Addison-Wesley, 1990.

[16] JOHNSSON, T. Efficient Computation of Lazy Evaluation. Proc. SIGPLAN’84. Sympo-sium on Compiler Construction ACM. Montreal, 1984.

[17] JOHNSSON, T. Lambda Lifting: Transforming Programs to Recursive Equations. AspenasWorkshop on Implementation of Functional Languages. Goteborg, 1985.

[18] JOHNSSON, T. Target Code Generation from G-Machine Code. Proc. Workshop onGraph Reduction, Santa Fe Lecture Notes on Computer science, Vol: 279 pp. 119-159.Spring-Verlag, 1986.

[19] JOHNSSON, T. Compiling Lazy Functional Languages. Ph.D. Thesis. Chalmers Universityof Technology, 1987.

[20] LANDIN, P. J. The Mechanical Evaluation of Expressions. Computer Journal, Vol. 6, 4.1964.

[21] LINS, Rafael Dueire et LIRA, Bruno O. ΓCMC: A Novel Way of Compiling FunctionalLanguages. J. Programming Languages 1:19-40; Chapmann & Hall. 1993.

[22] LINS, Rafael Dueire. O λ-Calculo, Computabilidade & Lingugens de Programacao. Notasde Curso. Recife-Pe, Nov. 1993.

[23] LINS, Rafael Dueire et all. Research Interests in Functional Programming. I Workshopon Formal Methods. UFRGS. Outubro, 1998.

[24] MACLENNAN, Bruce J. Functional Programming Practice. Addison-Wesley PublishingCompany, Inc. 1990.

[25] MEIRA, Sılvio Romero de Lemos. Introducao a programacao Funcional. VI Escola deComputacao. Campinas, 1988.

[26] OKASAKI, Chris. Purely Functional Data Structures. Cambridge University Press. 2003.

[27] PAULSON, Laurence C. ML for the Working Programmer. Cambridge University Press,1991.

[28] PEMMARAJU, Sriram et SKIENA, Steven. Computational Discrete Mathematics: Com-binatorics and Graph Theory with Mathematica. Cambridge University Press. 2003.

[29] PEYTON JONES, S. L. The Implementation of Functional Programming Languages. C.A. R. Hoare Series Editor. Prentice/Hall International. 1987.

[30] RABHI, Fethi et LAPALME, Guy. Algorithms: A Functional Programming Approach.2nd. edition. Addison-Wesley. 1999.

[31] SCHMIDT, D. A. Denotational Semantics. Allyn and Bacon, Inc. Massachusetts, 1986.

[32] SCOTT, D. Data Types as Lattices. SIAM Journal of Computing. Vol. 5,3. 1976.

[33] SKIENA, Steven S. et REVILLA, Miguel A. Programming Challenges: The ProgrammingContext Training Manual. Texts in Computer Science. Springer Science+Business Media,Inc. 2003.

[34] STOY, J. E. Denotational Semantics: The Scott-Strachey Approach to ProgrammingLanguage Theory. MIT Press, 1977.

154

Page 163: LF Apostila

[35] THOMPSON, Simon. Haskell: The Craft of Functional Programming. 2nd. Edition.Addison Wesley. 1999.

[36] TURNER, David A. A New Implementation Technique for Applicative Languages. Soft-ware Practice and Experience. Vol. 9. 1979.

[37] WADLER, Philip. Why no ones uses functional languages. Functional Programming.ACM SIGPLAN. 2004.

[38] WELCH, Peter H. The λ-Calculus. Course notes, The University of Kent at Canterbury,1982.

155