Vous êtes sur la page 1sur 163

FUNCIONAL

PROGRAMAC
AO
USANDO HASKELL

Programa
c
ao Funcional
Usando Haskell

Francisco Vieira de Souza


Licenciado em Matematica (1978) e Engenheiro Civil (1982) pela Universidade Federal
do Piau, Mestre (1994) e Doutor (2000) em Ciencia da Computacao pela Universidade
Federal de Pernambuco.
Professor do Departamento de Matem
atica (1986-1988), fundador (1987) e professor (desde
1987) do Departamento de Inform
atica e Estatstica da Universidade Federal do Piau.

UFPI/CCN/DIE
Teresina-Pi
Agosto de 2005

c
Copyright 2005,

Departamento de Inform
atica e Estatstica,
Centro de Ciencias da Natureza,
Universidade Federal do Piau.
Todos os direitos reservados.
A reproducao do todo ou parte deste
trabalho somente sera permitida para ns
educacionais e de pesquisa e com a expressa
autorizacao do autor.

c
Copyright 2005,

Departamento de Inform
atica e Estatstica,
Centro de Ciencias da Natureza,
Universidade Federal do Piau.
All rights reserved.
Reproduction of all or part of this work only
will be permitted for educational or research use
and with expressed permission of the author.

iii

Apresentac
ao
Esta Apostila representa a compilac
ao de v
arios t
opicos, desenvolvidos por pesquisadores de
renome, no campo da programac
ao funcional. Ela tem como objetivo servir de guia aos prossionais de Inform
atica que desejam ter um conhecimento inicial sobre o paradigma funcional de
forma geral e, em particular, sobre programac
ao usando Haskell. Ultimamente, Haskell tem se
tornado a linguagem funcional padr
ao do discurso, j
a existindo v
arios interpretadores e compiladores para ela, alem de v
arias ferramentas de an
alise de programas nela codicados (proles).
Para atingir este objetivo, acreditamos que o estudo deva ser acompanhado de algum conhecimento, mesmo que mnimo, sobre a fundamentac
ao destas linguagens e da forma como
elas s
ao implementadas. Este conhecimento proporciona ao leitor uma vis
ao das principais caractersticas e propriedades destas linguagens. Em particular, e importante entender porque as
tecnicas utilizadas na compilac
ao das linguagens imperativas n
ao se mostraram adequadas na
compilac
ao de linguagens funcionais.
Em 1978, John Backus advogou o paradigma funcional como o que oferecia a melhor soluc
ao
para a chamada crise do software. As linguagens funcionais s
ao apenas uma sintaxe mais
c
omoda para o -c
alculo. David Turner [36] mostrou, em 1979, que a l
ogica combinatorial
poderia ser extendida de forma a possibilitar a implementac
ao eciente de linguagens funcionais.
Esse trabalho provocou uma corrida em direc
ao `
a pesquisa nesta
area, gerando uma variedade
de tecnicas de implementac
ao destas linguagens.
Dentre estas tecnicas, uma que tem sido adotada, com resultados promissores, e a utilizac
ao
do -c
alculo como linguagem intermedi
aria entre a linguagem de alto nvel e a linguagem de
m
aquina. Os programas codicados em alguma linguagem funcional de alto nvel s
ao traduzidos
para programas em -c
alculo e destes para programas em linguagem de m
aquina. Neste caso, o
-c
alculo desempenha um papel semelhante ao que a linguagem Assembly exerce, como linguagem
de montagem, na compilac
ao de linguagens imperativas. Esta metodologia tem dado certo, uma
vez que j
a se conhecem tecnicas ecientes de traduc
ao de programas em -c
alculo para programas
execut
aveis, faltando apenas uma traduca
o eciente de programas codicados em uma linguagem
funcional de alto nvel para programas em -c
alculo.
Esta Apostila tem incio em seu primeiro Captulo tratando das caractersticas das linguagens funcionais, destacando suas vantagens em relac
ao `
as linguagens de outros paradigmas que
utilizam atribuic
oes destrutivas. Em seguida, e feita uma introduc
ao ao -c
alculo. Apesar do
car
ater introdut
orio, achamos ser suciente para quem quer dar os primeiros passos em direc
ao
a aprendizagem desta tecnica. Os Captulos subseq
`
uentes se referem todos a
` Programac
ao Funcional usando Haskell.
Por ser uma primeira tentativa, a Apostila contem erros e sua apresentac
ao did
atico-pedag
ogica deve ser revista. Neste sentido, agradecemos crticas construtivas que ser
ao objeto de
an
alise e reex
ao e, por isto mesmo, muito bem-vindas.
Teresina-Pi, agosto de 2005.
Francisco Vieira de Souza

iv

Conte
udo
Introdu
c
ao

viii

1 Programa
c
ao Funcional

11

1.1

Introducao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

11

1.2

Hist
orico . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

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

Denicao de funcoes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

20

1.9

Resumo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

25

2 -c
alculo

27

2.1

Introducao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

27

2.2

-expressoes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

28

2.3

A sintaxe do -calculo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

29

2.3.1

Aplicacao de funcao e curricacao . . . . . . . . . . . . . . . . . . . . . .

29

2.4

Funcoes e constantes pre-denidas . . . . . . . . . . . . . . . . . . . . . . . . . .

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

Conversao, reducao e abstracao . . . . . . . . . . . . . . . . . . . . . . . . . . . .

37

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

38

2.9

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

38

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

39

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

40

2.14 Resumo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

42

3 Programa
c
ao funcional em Haskell

43

3.1

Introducao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

43

3.2

Primeiros passos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

44

3.2.1

O interpretador Hugs . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

45

3.2.2

Identicadores em Haskell . . . . . . . . . . . . . . . . . . . . . . . . . . .

47

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

Tipos de dados em Haskell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

52

3.4.1

Os tipos primitivos da linguagem . . . . . . . . . . . . . . . . . . . . . . .

52

3.4.2

Programando com n
umeros e strings . . . . . . . . . . . . . . . . . . . . .

59

3.4.3

Os tipos de dados estruturados de Haskell . . . . . . . . . . . . . . . . . .

60

3.4.4

Escopo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

61

3.4.5

C
alculos: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

62

3.4.6

Projeto de programas . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

65

3.4.7

Provas de programas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

65

Resumo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

69

3.3

3.4

3.5

4 O tipo Lista
4.1

Funcoes sobre listas

71
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

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

Polimorsmo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

90

4.5.1

Tipos vari
aveis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

91

4.5.2

O tipo mais geral . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

91

4.6

Inducao estrutural . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

92

4.7

Composicao de funcoes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

95

4.7.1

96

4.5

Composicao avancada . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
vi

4.7.2
4.8

4.9

Esquema de provas usando composicao . . . . . . . . . . . . . . . . . . . .

96

Aplicacao parcial . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

98

4.8.1

Secao de operadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

99

4.8.2

Curricacao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

99

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.3

5.4

5.2.1

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

5.2.2

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

5.2.3

Assinaturas e inst
ancias . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112

5.2.4

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

5.2.5

As classes pre-denidas em Haskell . . . . . . . . . . . . . . . . . . . . . . 113

Tipos algebricos

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115

5.3.1

Como se dene um tipo algebrico? . . . . . . . . . . . . . . . . . . . . . . 116

5.3.2

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

5.3.3

Derivando inst
ancias de classes . . . . . . . . . . . . . . . . . . . . . . . . 118

5.3.4

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

5.3.5

Recursao m
utua . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120

5.3.6

Tipos algebricos polim


orcos . . . . . . . . . . . . . . . . . . . . . . . . . 121

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


5.4.1

Valores ctcios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124

5.4.2

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

5.5

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

5.6

M
odulos em Haskell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128

5.7

5.6.1

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

5.6.2

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

5.6.3

O m
odulo main . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129

5.6.4

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

5.6.5

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

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

5.8

5.9

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


5.8.1

Express
oes ZF (revisadas) . . . . . . . . . . . . . . . . . . . . . . . . . . . 140

5.8.2

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

5.8.3

Listas innitas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141

Resumo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142

6 Programa
c
ao com a
c
oes em Haskell

145

6.1

Introducao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145

6.2

Entrada e Sada em Haskell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146

6.3

6.2.1

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

6.2.2

Operacoes de sada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147

6.2.3

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

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

Refer
encias Bibliogr
aficas

152

viii

Introdu
c
ao
Functional languages privide a framework
in 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 ele
comenta sobre a pouca utilizacao das linguagens funcionais na Ind
ustria e ambientes comerciais
[37]. Para ele, dizer que ninguem usa linguagem funcional, e um exagero. As chamadas telef
onicas no Parlamento Europeu s
ao roteadas por um programa escrito em Erlang, a linguagem
funcional da Ericsson. A rede Cornell distribui CDs virtuais usando o sistema Esemble, escrito
em CAML e a Polygram vende CDs na Europa usando Natural Expert, da Software AG. As
linguagens Erlang (www.erlang.se) e ML Works de Harlequin (www.harlequin.com) apresentam
um extensivo ambiente de suporte ao usu
ario. Alem disso, as linguagens funcionais s
ao mais
adequadas a` construcao de provadores de teoremas, incluindo o sistema HOL que foi utilizado
na depuracao do projeto de multiprocessadores da linha HP 9000.
Ainda segundo Wadler, as linguagens funcionais produzem um c
odigo de maquina com uma
melhoria de uma ordem de magnitude e, nem sempre, estes resultados s
ao mostrados. Normalmente, se mostram fatores de 4. Mesmo assim, um c
odigo que e quatro vezes menor, quatro vezes
mais rapido de ser escrito ou quatro vezes mais facil de ser mantido n
ao pode ser jogado fora.
Para ele, os principais fatores que inuenciam na escolha de uma linguagem de programacao
sao:
Compatibilidade. Os sistemas n
ao sao mais construdos monoliticamente, como eram
no passado. Atualmente, eles s
ao escritos de forma modular. Os modulos sao construdos
por usu
arios em tempos e locais possivelmente diferentes e sao ligados atraves de interfaces
bem denidas. Muitas linguagens funcionais j
a apresentam facilidades para a construcao
de grandes softwares com formas bem adequadas de construcao de modulos e algumas
necessario acabar com o
linguagens funcionais ainda n
ao oferecem estas facilidades. E
isolamento das linguagens funcionais e incorporar a elas facilidades para a comunicacao
entre programas funcionais e programas codicados em outras linguagens pertencentes
a outros paradigmas. A Ind
ustria da Computacao esta comecando a distribuir padr
oes
como CORBA e COM para suportar a construcao de software a partir de componentes
reutiliz
aveis. Atualmente, os programas em Haskell ja podem ser empacotados como um
componente COM e qualquer componente COM pode ser chamado a partir de Haskell.
Entre outras aplicacoes, isto permitiu que Haskell fosse utilizada como linguagem para a
construcao do Internet Explorer, o browser da Microsoft.
Bibliotecas. Muitos usu
arios escolheram Tcl, atrados, principalmente, pela biblioteca
gr
aca Tk. Muito pouco da atratividade de Java tem a ver com a linguagem em si,
1

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

e sim com as bibliotecas associadas, usadas na construcao de gr


acos, banco de dados,
interfaceamento, telefonia e servidores. Apesar de ainda n
ao existirem muitas bibliotecas
gr
acas para as linguagens funcionais, muito esforco tem sido feito nesta direcao, nos
u
ltimos 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 tornam
suas bibliotecas f
aceis de serem construdas.
Portabilidade. Inegavelmente C e C++ tem sido preferidas em muitos projetos. No
entanto, muito desta preferencia nao se deve ao fato de C gerar um codigo mais rapido
que o codigo gerado pelas linguagens funcionais, apesar de, normalmente, se vericar esta
diferenca de desempenho. Na realidade, esta preferencia se deve mais `a portabilidade
ineg
avel de C. Sabe-se que os pesquisadores em Lucent teriam preferido construir a linguagem PRL para Banco de Dados usando SML, mas escolheram C++, porque SML n
ao
estava disponvel no mainframe Amdahl, onde deveria ser utilizada. Por outro lado, as
tecnicas de implementacao de linguagens utilizando m
aquinas abstratas tem se tornado
muito atrativas para linguagens funcionais [21] e tambem para Java. Isto se deve muito
ao fato de que escrever a maquina abstrata em C a torna muito mais f
acil de ser portada
para uma grande variedade de arquiteturas.
Disponibilidade. Alguns compiladores s
ao muito difceis de serem instalados. Por exemplo, GHC (Glasgow Haskell Compiler) era considerado uma aventura por alguns usu
arios
que tentavam instal
a-lo. Ainda existem poucas linguagens funcionais comerciais e isto
torna difcil um c
odigo estavel e um suporte con
avel. Alem do mais, as linguagens funcionais estao em permanente desenvolvimento e, portanto, estao sempre em transformacoes.
Empacotamento. Muitas linguagens funcionais seguem a tradicao de LISP, de sempre
realizar suas implementacoes atraves do loop read-eval-print. Apesar da conveniencia,
e essencial desenvolver habilidades para prover alguma forma de conversao de programas
funcionais em programas de aplicacao standalone. Muitos sistemas j
a oferecem isto, no
entanto, incorporam o pacote de runtime completo `a biblioteca e isto implica na exigencia
de muita memoria.
Ferramentas. Uma linguagem para ser utiliz
avel necessita de ferramentas para depuracao
e proler. Estas ferramentas sao f
aceis de serem construdas para linguagens estritas, no
entanto, s
ao muito difceis de serem construdas para linguagens lazy, onde a ordem de
avaliacao n
ao e conhecida a priori. Verica-se uma excecao em Haskell, onde muitas
ferramentas de proler j
a estao disponveis.
Treinamento. Para programadores imperativos e muito difcil programar funcionalmente.
Uma solucao imperativa e mais facil de ser entendida e de ser encontrada em livros ou
artigos. Uma solucao funcional demora mais tempo para ser criada, apesar de muito mais
elegante. Por este motivo, muitas linguagens funcionais atuais proveem um escape para o
estilo imperativo. Isto pode ser vericado em ML que n
ao e considerada uma linguagem
funcional pura, porque permite atribuicoes destrutivas. Haskell e uma linguagem funcional
pura, mas consegue imitar as atribuicoes das linguagens imperativas utilizando uma teoria
funcional complexa que e a semantica de acoes, implementadas atraves de monadas2 .
Popularidade. Se um gerente escolher uma linguagem funcional para ser utilizada em
um projeto e este falhar, provavelmente ele ser
a crucicado. No entanto, se ele escolher C
ou C++ e n
ao tiver sucesso, tem a seu favor o argumento de que o sucesso de C++ ja foi
vericado em in
umeros casos e em varios locais.
2

A sem
antica de aco
es e um tema tratado no Captulo 6. Os m
onadas n
ao s
ao aqui tratados, uma vez que seu
estudo requer, como pre-requsito, o conhecimento aprofundado sobre implementaca
o de linguagens funcionais.

Desempenho. Ha uma decada atr


as, os desempenhos dos programas funcionais eram
bem menores que os dos programas imperativos, mas isto tem mudado muito ultimamente.
Hoje, os desempenhos de muitos programas funcionais s
ao melhores ou pelo menos estao
em pes de igualdade com seus correspondentes em C. Isto depende da aplicacao. Java
tem uma boa aceitacao e, no entanto, seu desempenho e muito inferior a C, na grande
maioria das aplicacoes. Na realidade, existem linguagens com alto desempenho que n
ao
sao muito utilizadas e existem linguagens com desempenho mediano com alta taxa de
utilizacao. Desempenho e um fator importante, mas n
ao tem se caracterizado como um
fator decisivo na escolha de uma linguagem.
Resumidamente, existem muitos fatores que desencorajam a escolha de linguagens funcionais como forma de se codicar programas. Para ser fortemente utilizada, uma linguagem deve
suportar trabalho interativo, possuir bibliotecas extensas, ser altamente port
avel, ter uma implementacao estavel e f
acil de ser instalada, ter depuradores e prolers, ser acompanhada de
cursos 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 linguagens
funcionais, por exemplo Haskell. Para ele o que ainda existe e um preconceito injustic
avel por
parte de alguns programadores de outros paradigmas de programacao. No entanto, para a
felicidade e consolo dos admiradores das linguagens funcionais, esta cultura tem se modicado,
e de forma r
apida, ao longo dos u
ltimos anos.
A nosso ver, o que existe mesmo e a falta de informacao e conhecimento do que realmente e
programacao funcional e quais as suas vantagens e desvantagens em relacao `a programacao em
outros paradigmas. Esta Apostila tenta prover subsdios para que seus usu
arios possam iniciar
um processo de discussao sobre o tema. Para desencadear este processo, e necessario comecar
pelo entendimento da relacao entre a programacao funcional e a programacao estruturada.

Princpios da programa
c
ao estruturada
Hoare enumerou seis princpios fundamentais da estruturacao de programas [24]:
1. Transpar
encia de significado. Este princpio arma que o signicado de uma express
ao,
como um todo, pode ser entendido em termos dos signicados de suas sub-express
oes.
Assim, o signicado da express
ao E + F depende simplesmente dos signicados das subexpressoes E e F, independente das complicacoes de cada uma delas.
2. Transpar
encia de prop
ositos. Textualmente, Hoare diz: o prop
osito de cada parte
consiste unicamente em sua contribuic
ao para o prop
osito do todo. Assim, em E + F,
ou
nico prop
osito de E e computar o n
umero que sera o operando esquerdo do operador
+. Isto signica que seu prop
osito nao inclui qualquer efeito colateral.
3. Independ
encia das partes. Este princpio apregoa que os signicados de duas partes,
n
ao sobrepostas, podem ser entendidos de forma completamente independente, ou seja, E
pode ser entendida independentemente de F e vice versa. Isto acontece porque o resultado
computado por um operador depende apenas dos valores de suas entradas.
4. Aplica
c
oes recursivas. Este princpio se refere ao fato de que as express
oes aritmeticas
sao construdas pela aplicacao recursiva de regras uniformes. Isto signica que se n
os
sabemos 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 cada
operacao aritmetica tem apenas uma sada e apenas uma ou duas entradas. Alem disso,
3

cada uma das entradas e sadas e um valor simples. Assim, a interface entre as partes e
clara, pequena e bem controlada.
6. Estruturas manifestas. Este princpio se refere ao fato de que os relacionamentos estruturais entre as partes de uma express
ao aritmetica seja obvio. Uma expressao e uma
sub-expressao de uma expressao se estiver textualmente envolvida nela. Tambem duas expressoes nao estao estruturalmente relacionadas se elas nao se sobrepuzerem de qualquer
forma.
Estas caractersticas sao verdadeiras em relacao a` programacao estruturada, no entanto, nem
sempre elas sao assim entendidas. A este respeito, John Hughes fez algumas reexoes, analisando a import
ancia da programacao estruturada [15], fazendo um paralelo com as linguagens
funcionais. Em sua an
alise, ele cita que quando se pergunta a alguem o que e programacao
estruturada, normalmente se tem, uma ou mais, das seguintes respostas:
e uma programacao sem gotos,
e uma programacao onde os blocos n
ao tem entradas ou sadas m
ultiplas ou
e uma programacao onde os programas nela escritos s
ao mais trat
aveis matematicamente.
Apesar de todas as respostas acima serem corretas, no que se refere `a caracterizacao deste tipo
de programacao, elas nao sao conclusivas. Aludem ao que a programacao estruturada n
ao e ,
mas nao dizem o que realmente e a programacao estruturada. Na realidade, uma resposta
coerente para a pergunta sobre a programacao estruturada pode ser: programas estruturados
s
ao programas construdos de forma modular.
Esta e uma resposta armativa e que atinge o cerne da quest
ao. A construcao de programas
modulares e responsavel pela grande melhoria na construcao de software, sendo a tecnica respons
avel pelo not
orio aumento de produtividade de software que ultimamente tem se vericado.
Este aumento de produtividade se verica porque:
modulos pequenos podem ser codicados mais facilmente e mais rapidamente,
modulos de prop
osito geral podem ser reutilizados, ou seja, maior produtividade e
os modulos podem ser testados e compilados de forma independente, facilitando muito a
depuracao 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 sao
mais faceis de serem encontradas. No entanto, estas pequenas solucoes devem ser combinadas
para representar uma solucao para o problema original como um todo.
Modula II, Ada, Pascal, C, C++, Standard ML, Haskell, Java, Eiel e todas as modernas linguagens de programacao, independente do paradigma utilizado, foram projetadas ou adaptadas
depois para serem modulares.

O que
e uma linguagem funcional?
Ainda segundo Hughes, um caso semelhante acontece quando se pergunta a alguem sobre o
que e programacao funcional. Normalmente, se tem como resultado uma ou mais das seguintes
respostas:
4

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


e uma linguagem que n
ao tem side eects,
e uma linguagem em que a ordem de execucao e irrelevante, ou seja, n
ao precisa analisar
o uxo de controle,
e uma linguagem onde pode-se substituir, a qualquer tempo, vari
aveis por seus valores
(transparencia referencial) ou
e uma linguagem cujos programas nela escritos sao mais trat
aveis matematicamente.
Estas respostas tambem nao sao conclusivas, da mesma forma que as respostas dadas a`
questao sobre programacao estruturada. E qual seria uma resposta armativa e denitiva?
Usando a resposta anterior como base, pode-se dizer que as linguagens funcionais s
ao altamente
modulariz
aveis..
Esta e uma resposta armativa e que necessita apenas de mais um complemento para que
a questao que respondida em seu todo. Este complemento se refere a`s caractersticas que
as linguagens funcionais apresentam e que proporcionam esta melhoria na modularidade dos
sistemas. Estas caractersticas podem ser sumarizadas na seguinte observacao: a programac
ao
funcional melhora a modularidade, provendo m
odulos menores, mais simples e mais gerais,
atraves das func
oes de alta ordem e lazy evaluation.
Estas duas caractersticas, vericadas apenas nas linguagens funcionais, e que sao respons
aveis pela grande modularidade por elas proporcionada. Dessa forma, as linguagens funcionais sao altamente estruturadas e se candidatam, com grande chance de exito, como solucoes
para a t
ao propalada crise do software dos anos 80.
Para continuar esta viagem pelo mundo da programacao funcional, tentando entender sua
fundamentacao teorica e necessario conhecer tambem como as linguagens s
ao implementadas.
Inicialmente vamos nos referir a` implementacao de linguagens tradicionais e depois particularizaremos para o caso das funcionais.

O processo de compila
c
ao de linguagens
Programar e modelar problemas do mundo real ou imagin
ario em um computador, usando algum
paradigma de programacao. Desta forma, os problemas sao modelados em um nvel de abstracao
bem mais alto atraves de especicacoes formais, feitas utilizando uma linguagem de especicacao
formal. Existem varias linguagens de especicacao formal: Lotos, Z, VDM, Redes de Petri, entre
outras. A escolha de uma delas esta diretamente ligada ao tipo da aplicacao e `a experiencia
do programador. Por exemplo, para especicar dispositivos de hardware e mais natural se
usar Redes de Petri, onde pode-se vericar a necessidade de sincronizacao e podem ser feitas
simulacoes para a an
alise de desempenho ou detectar a existencia, ou n
ao, de inconsistencias.
Muitas ferramentas gr
acas ja existem e sao utilizadas na analise de desempenho de dispositivos especicados formalmente e podem ser feitos prototipos r
apidos para se vericar a adequabilidade das especicacoes `as exigencias do usu
ario, podendo estes prot
otipos serem parte
integrante do contrato de trabalho entre o programador e o contratante. Mais importante que
isto, a especicacao formal representa um prova da corretude do programa e que ele faz exatamente o que foi projetado para fazer. Isto pode ser comparado com a garantia que um usu
ario
tem quando compra um eletrodomestico em uma loja. Esta garantia nao pode ser dada pelos
testes, uma vez que eles so podem vericar a presenca de erros, nunca a ausencia deles. Os
testes representam uma ferramenta importante na ausencia de uma prova da corretude de um
5

programa, mas n
ao representam uma prova. O uso de testes requer que eles sejam bem projetados e de forma objetiva, para que tenham a sua existencia justicada. Com a especicacao
pronta, 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, este
processo e tao somente uma traducao, dependendo da experiencia do programador com a linguagem de programacao escolhida. No caso das linguagens funcionais, este processo e muito natural,
uma vez que as especicacoes formais sao apenas denicoes implcitas de funcoes, restanto apenas a traducao destas denicoes implcitas para denicoes explcitas na linguagem funcional
escolhida3 . Resta agora a traducao destes programas em linguagens de alto nvel para programas executaveis em linguagem de maquina, sendo este o papel do compilador. Este processo
esta mostrado na Figura 1.
A compilacao de programas codicados em linguagens imperativas, normalmente, e feita em
duas etapas [1]. Na primeira delas, e feita uma traducao do programa escrito em linguagem de
alto nvel para um programa codicado em linguagem intermedi
aria, chamada de linguagem de
montagem (Assembly). Na etapa seguinte, e feita a traducao do programa em linguagem de
montagem para o programa execut
avel, em linguagem de maquina. Este processo est
a mostrado
na Figura 2.

Figura 2: Processo de compilacao das linguagens imperativas.


3

Estas formas de definica


o de funco
es s
ao mostradas no Captulo 1 desta Apostila.

Esta mesma metodologia foi tentada por alguns pesquisadores na traducao de programas codicados em linguagens funcionais para programas em linguagem de m
aquina, mas os resultados
n
ao foram animadores [22]. Os c
odigos executaveis gerados eram todos de baixo desempenho.
Por este motivo, os pesquisadores da area de implementacao de linguagens funcionais se viram
obrigados buscar outras alternativas de compilacao para estas linguagens.

A implementa
c
ao de linguagens funcionais
Como ja mencionado anteriormente, as linguagens funcionais apresentam caractersticas u
nicas,
como as funcoes de alta ordem, polimorsmo e lazy evaluation. Estas caractersticas sao proporcionadas por particularidades apresentadas pela programacao aplicativa ou funcional. Estas particularidades s
ao: a transparencia referencial, a propriedade de Church-Rosser, a independencia
na ordem de avaliacao e as interfaces manifestas, que serao objeto de estudo no Captulo 1
desta Apostila. Para que estas particularidades estejam presentes, e necessario que, durante a
execucao, muitas estruturas permanecam ativas na heap para que possam ser utilizadas mais
tarde. Estas caractersticas tem impedido a criacao de codigos executaveis enxutos e de bons
desempenhos.
Por este motivo, outras tecnicas de implementacao foram pesquisadas. Uma que tem apresentado resultados promissores consiste na traducao de programas codicados em linguagens
funcionais para programas em uma linguagem intermedi
aria, como na traducao das linguagens
4
aria, em vez da linguagem Asimperativas, mas utilizando -calculo como linguagem intermedi
sembly [21]. J
a existem metodos ecientes de traducao de programas codicados no -calculo
para programas em linguagem de m
aquina. Dessa forma, o problema agora se restringe a`
traducao dos programas escritos nas linguagens funcionais para programas em -calculo. Este
processo esta mostrado na Figura 3.

Figura 3: Um processo de compilacao das linguagens funcionais.


A escolha do -calculo como linguagem intermedi
aria entre as linguagens funcionais e o
codigo 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 sucientemente poderosa para expressar todos os programas
funcionais.
4

-c
alculo e uma teoria de funco
es que ser
a vista no Captulo 2 desta Apostila.

M
aquinas abstratas
Uma tecnica usada com sucesso na implementacao de linguagens funcionais consiste na implementacao do -calculo usando a reducao das -expressoes para -expressoes mais simples, ate
atingir uma forma normal, se ela existir. O resultado desta tecnica foi um sistema que cou
conhecido na literatura como m
aquinas abstratas. A primeira m
aquina abstrata foi desenvolvida
por Peter Landin em 1964 [20], que ganhou o alcunha de m
aquina SECD, devido ao seu nome
(Stack, Environment, Code, Dump). A maquina SECD usa a pilha S para a avaliacao das
-expressoes Codicadas, utilizando o ambiente E.
Uma otimizacao importante na m
aquina SECD foi transferir alguma parcela do tempo de
execucao para o tempo de compilacao. No caso, isto foi feito transformando as expressoes com
notacao inxa para a notacao polonesa reversa que e adequada para ser executada em pilha,
melhorando o ambiente de execucao. Para diferenciar da m
aquina SECD, esta maquina foi
chamada de SECD2.
Em 1979, David Turner [36] desenvolveu um processo de avaliacao de expressoes em SASL
(uma linguagem funcional) usando o que cou conhecida como a m
aquina de combinadores.
5
oes, em vez de
Ele utilizou os combinadores S, K e I do -calculo para representar express
-expressoes, utilizando para isto um dos combinadores acima citados, sem vari
aveis ligadas [7].
Turner traduziu diretamente as express
oes em SASL para combinadores, sem passar pelo estagio
intermedi
ario das -expressoes. A maquina de reducao de Turner foi chamada de M
aquina
de Redu
c
ao SK e utilizava a reducao em grafos como metodo para sua implementacao. Esta
maquina teve um impacto muito intenso na implementacao de linguagens aplicativas, pelo ganho
em eciencia, uma vez que ela n
ao utilizava o ambiente da maquina SECD, mas tirava partido
do compartilhamento que conseguia nos grafos de reducao.
Um outro pesquisador que se tornou famoso no desenvolvimento de m
aquinas abstratas foi
Johnsson, a partir de 1984, quando ele deu incio a uma serie de publicacoes [16, 17, 18] sobre este
tema, culminando com sua Tese de Doutorado, em 1987 [19], onde ele descreve uma maquina
abstrata baseada em supercombinadores que eram combinadores abstrados do programa de
usu
ario para algumas necessidades particulares. A maquina inventada por Johnsson cou conhecida pela M
aquina G e se caracterizou por promover uma melhoria na granularidade dos
programas, que era muito na na m
aquina de reducao de Turner. Uma otimizacao importante
desta maquina foi a utilizacao de uma segunda pilha na avaliacao de expressoes aritmeticas ou
outras expressoes estritas.

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


Varias outras m
aquinas abstratas foram construdas com bons resultados. Entre elas podem
ser citadas a maquina GMC e a m
aquina , idealizadas por Rafael Lins, da Universidade Federal
5

Combinadores e supercombinadores s
ao temas do -c
alculo, objeto de estudo do Captulo 2, deste trabalho.

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 codicado em SASL e traduzido para um programa
em 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 reconhecidamente port
ateis e ecientes. A maquina CMC e baseada nos combinadores categoricos
que se fundamentam na Teoria das Categorias Cartesianas Fechadas, recentemente utilizada em
diversas areas da Computacao, sendo hoje um tema padr
ao do discurso, nos grandes encontros
e eventos na area da Inform
atica [9].

Esta Apostila
Esta Apostila e composta desta Introducao e 6 (seis) Captulos. Nesta Introducao, e colocada,
de forma resumida, a import
ancia das linguagens funcionais e a necessidade de estudar o calculo e justicar sua escolha como a linguagem intermedi
aria entre as linguagens funcionais
necessario saber que as linguagens funcionais s
e as linguagens de m
aquina. E
ao importantes
porque aumentam a modularidade dos sistemas atraves das funcoes de alto nvel e do mecanismo
de avaliacao preguicosa.
O Captulo 1 e dedicado a` fundamentacao das linguagens funcionais, abordando as principais diferencas entre elas e as linguagens de outros paradigmas. O mundo das linguagens de
programacao e dividido entre o mundo das express
oes e o mundo das atribuicoes, evidenciando
as vantagens do primeiro mundo em relacao ao segundo.
No Captulo 2, e introduzido o -calculo, sua evolucao hist
orica e como ele e usado nos dias
atuais. A teoria e colocada de maneira simples e introdutoria, dado o objetivo da Apostila.
No Captulo 3, inicia-se a programacao em Haskell. Sao mostrados seus construtores e uma
serie de exemplos, analisando como as funcoes podem ser construdas em Haskell. S
ao mostrados
os tipos de dados primitivos adotados em Haskell e os tipos estruturados mais simples que sao as
tuplas. No Captulo, tambem sao mostrados os esquemas de provas de programas, juntamente
com varios exerccios, resolvidos ou propostos.
O Captulo 4 e dedicado a` listas em Haskell. Este Captulo se torna necessario, dada a
import
ancia que este tipo de dado tem nas linguagens funcionais. Neste Captulo, s
ao mostradas
as compreensoes ou expressoes ZF e tambem e mostrada a composicao de funcoes como uma
caracterstica apenas das linguagens funcionais, usada na construcao de funcoes. Um tema
importante e que e discutido neste Captulo se refere `as formas de provas da corretude de
programas em Haskell, usando inducao estrutural sobre listas. No Captulo s
ao mostrados
v
arios exemplos resolvidos e, ao nal, s
ao colocados v
arios exerccios `a apreciacao do leitor.
No Captulo 5, s
ao mostradas as type class, como formas de incluir um determinado tipo
de dados em em uma classe de tipos que tenham funcoes em comum, dando origem `a sobrecarga
como forma de polimorsmo. Tambem sao mostrados os tipos de dados algebricos, o sistema de
modulos adotado em Haskell, os tipos de dados abstratos e o tratamento de excecoes. O Captulo
termina com uma revisao sobre o sistema de avaliacao lazy, notadamente na construcao de listas
potencialmente innitas.
O Captulo 6 e dedicado a`s operacoes de entrada e sada em Haskell, evidenciado o uso de
arquivos ou dispositivos como sada ou como entrada. Este processo em Haskell e feito atraves
do mecanismo de acoes, cuja semantica representa o conte
udo principal do Captulo.
A Apostila termina com as principais referencias bibliogracas consultadas durante s sua
elaboracao.

10

Captulo 1

Programa
c
ao Funcional
We can now see that in a lazy implementation
based on suspensions, we can treat every function in the same way.
Indeed, all functions are treated as potentially non-strict
and their argument is automatically suspended.
Later, is and when it is needed, it will be
unsuspended (strictly evaluated).
(Antony D. T. Davie in [7])

1.1

Introdu
c
ao

A programacao funcional teve incio antes da invencao dos computadores eletronicos. No incio
do seculo XX, muitos matematicos estavam preocupados com a fundamentacao matematica, em
particular, queriam saber mais sobre os conjuntos innitos. Muito desta preocupacao aconteceu
por causa do surgimento, no nal do seculo XIX, de uma teoria que armava a existencia de varias
ordens de innitos, desenvolvida por George Cantor (1845-1918) [24]. Muitos matematicos, como
Leopold Kronecker (1823-1891), questionaram a existencia destes objetos e condenaram a teoria
de Cantor como pura enrolacao. Estes matematicos defendiam que um objeto matematico
so poderia existir se, pelo menos em princpio, pudesse ser construdo. Por este motivo, eles
caram conhecidos como construtivistas.
Mas o que signica dizer que um n
umero, ou outro objeto matem
atico, seja construtvel?
Esta ideia foi desenvolvida lentamente, ao longo de muitos anos. Guiseppe Peano (1858-1932),
um matematico, l
ogico e linguista, escreveu Formulaire de Mathematique (1894-1908), onde
mostrou como os n
umeros naturais poderiam ser construdos atraves de nitas aplicacoes da
funcao sucessor. Comecando em 1923, Thoralf Skolen (1887-1963) mostrou que quase tudo
da teoria dos n
umeros naturais poderia ser desenvolvido construtivamente pelo uso intensivo
de denicoes recursivas, como as de Peano. Para evitar apelos questionaveis sobre o innito,
pareceu razoavel chamar um objeto de construtvel se ele pudesse ser construdo em um n
umero
nito de passos, cada um deles requerendo apenas uma quantidade nita de esforco. Assim, nas
primeiras decadas do seculo XX, j
a existia consideravel experiencia sobre as denicoes recursivas
de funcoes sobre os n
umeros naturais.
A cardinalidade (quantidade de elementos) dos conjuntos nitos era f
acil de ser conhecida,
uma vez que era necessario apenas contar seus elementos. Ja para os conjuntos innitos, esta
tecnica nao podia ser aplicada. Inicialmente, era necess
ario denir o que era realmente um
conjunto innito. Foi denido que um conjunto era innito se fosse possvel construir uma
correspondencia biunvoca entre ele e um subconjunto pr
oprio dele mesmo. Foram denidos os
conjuntos innitos enumer
aveis, que foram caracterizados pelos conjuntos innitos para os quais
11

fosse possvel construir uma correspondencia biunvoca com o conjunto dos n


umeros naturais, N.
Assim, todos os conjuntos innitos enumer
aveis tinham a mesma cardinalidade, que foi denida
umeros inteiros, Z, tambem e 0 , uma vez
por 0 1 . Assim, a cardinalidade do conjunto dos n
que e possvel construir uma correspondencia biunvoca entre Z e N. Os conjuntos innitos com
os quais nao fosse possvel estabelecer uma correspondencia biunvoca entre eles e N, foram
chamados de innitos n
ao enumer
aveis. A cardinalidade do conjuntos dos n
umeros reais, R,
que e innito n
ao enumer
avel, e 20 .

1.2

Hist
orico

Na decada de 1930, existiram muitas tentativas de formalizacao do construtivismo, procurando


caracterizar o que era camputabilidade efetiva, ou seja, procurava-se saber o que realmente podia
ser computado. Uma das mais famosas tentativas foi a denicao de Turing sobre uma classe
de maquinas abstratas, que caram conhecidas como m
aquinas de Turing, que realizavam
operacoes de leitura e escritas sobre uma ta de tamanho nito. Outra tecnica, baseada mais
diretamente nos trabalhos de Skolen e Peano, consistia no uso de func
oes recursivas gerais,
devida a G
odel. Uma outra tecnica, com implicacao importante na programacao funcional, foi
a criacao do -calculo, desenvolvido por Church e Kleene, no incio da decada de 1930. Outra
nocao de computabilidade, conhecida como Algoritmos de Markov, tambem foi desenvolvida
nesta mesma epoca. O que e importante e que todas estas nocoes de computabilidade foram
provadas serem equivalentes. Esta equivalencia levou Church, em 1936, a propor o que cou
ao era comput
avel se ela
conhecida como a Tese de Church 2 , onde ele armava que uma func
fosse primitiva recursiva [5].
Isto signica que, j
a na decada anterior `a decada da invencao do computador eletronico,
muitos matematicos e logicos j
a haviam investigado, com profundidade, a computabilidade de
funcoes e identicado a classe das funcoes computaveis como a classe das funcoes primitivas
recursivas.
O pr
oximo invento importante na historia da programacao funcional foi a publicacao de John
McCarthy, em 1960, sobre LISP. Em 1958, ele investigava o uso de operacoes sobre listas ligadas
para implementar um programa de diferenciacao simbolica. Como a diferenciacao e um processo
recursivo, McCarthy sentiu-se atrado a usar funcoes recursivas e, alem disso, ele tambem achou
conveniente passar funcoes como argumentos para outras funcoes. McCarthy vericou que o
-calculo provia uma notacao muito conveniente para estes prop
ositos e, por isto mesmo, ele
resolveu usar a notacao de Church em sua programacao.
Em 1958, foi iniciado um projeto no MIT com o objetivo de construir uma linguagem de
programacao que incorporasse estas ideias. O resultado cou conhecido como LISP 1, que foi
descrita por McCarthy, em 1960, em seu artigo Recursive Functions of Symbolic Expressions
and Their Computation by Machine [24]. Este artigo mostrou como v
arios programas complexos podiam ser expressos por funcoes puras operando sobre estruturas de listas. Este fato e
caracterizado, por alguns pesquisadores, como o marco inicial da programacao funcional.
No nal da decada de 1960 e incio da decada de 1970, um grande n
umero de cientistas
da Computacao comecaram a investigar a programacao com funcoes puras, chamada de programac
ao aplicativa, uma vez que a operacao central consistia na aplicacao de uma funcao a
seu argumento. Em particular, Peter Landin (1964, 1965 e 1966) desenvolveu muitas das ideias
centrais para o uso, notacao e implementacao das linguagens de programacao aplicativas, sendo
importante destacar sua tentativa de traduzir a denicao de uma linguagem nao funcional, Algol
1

e uma letra do alfabeto a


rabe, conhecida por Aleph.
Na realidade, n
ao 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.
2

12

60, para o -calculo. Baseados neste estudo, Strachey e Scott construiram um metodo de denicao da semantica de linguagens de programacao, conhecido como sem
antica denotacional.
Em essencia, a semantica denotacional dene o signicado de um programa, em termos de um
programa funcional equivalente.
No entanto, a programacao aplicativa, que havia sido investigada por um reduzido n
umero
de 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 severas
crticas `as linguagens de programacao convencionais, sugerindo a criacao de um novo paradigma
de programacao. Ele prop
os o paradigma chamado de programac
ao funcional que, em essencia,
e a programacao aplicativa com enfase no uso de funcionais, que s
ao funcoes que operam sobre
outras funcoes. Muitos dos funcionais de Backus foram inspirados pela linguagem APL, uma
linguagem imperativa, projetada na decada de 1960, que provia operadores poderosos, sem
atribuicao, sobre estruturas de dados. A partir desta publicacao, o n
umero de pesquisadores na
area de linguagens de programacao funcional tem aumentado signicativamente.

1.3

Programa
c
ao com express
oes

Para MacLennan [24] qualquer linguagem de programacao se divide em dois mundos, a saber:
o mundo das express
oes (aritmeticas, relacionais, booleanas, etc.) e o mundo das atribuicoes.
No primeiro caso, o u
nico objetivo e encontrar o valor de uma expressao atraves de um processo
de avaliac
ao. J
a no segundo caso, o mundo das atribuicoes e dividido em dois tipos. O
primeiro altera o controle do uxo de um programa, usando comandos de selecao, como if, for,
while, repeat, goto e chamadas a procedimentos. O segundo tipo de atribuicao altera o estado
da memoria (principal ou secund
aria) do computador. Nos dois tipos, a palavra chave e alterar
alguma coisa; no primeiro tipo, altera o uxo de controle e, no segundo, altera o estado da
maquina.
No mundo das atribuicoes, a ordem em que as coisas sao feitas tem importancia fundamental.
Por exemplo, e seq
uencia
i = i + 1;
a = a i;
tem um efeito diferente da seq
uencia, 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 direito
do sinal de atribuicao contem uma sub-expressao em comum (2 a y) e qualquer compilador,
com um mnimo de otimizacao, transformaria este fragmento de c
odigo, na seguinte forma:
t = 2 a y;
z = (t + b) (t + c);
No mundo das express
oes, e seguro promover esta otimizacao porque qualquer sub-express
ao,
no caso 2 a y, sempre tem o mesmo valor. Analisemos agora, uma situacao similar no mundo
das atribuicoes.
Sejam as express
oes
y = 2 a y + b;

z = 2 a y + c;

onde tambem vericamos uma sub-expressao em comum (2 a y). Se for realizada a mesma
fatoracao anterior, teremos:
13

t = 2 a y;
y = t + b; z = t + c;
Esta otimizacao altera o valor da vari
avel z, porque os valores de y sao diferentes nas duas
ocorrencias da sub-express
ao 2*a*y. Portanto, n
ao e possvel realizar esta otimizacao. Apesar
da an
alise sobre a existencia, ou n
ao, de dependencia entre sub-expressoes poder ser feita por um
compilador, isto requer tecnicas sosticadas de analise de dependencias no uxo. Tais an
alises,
normalmente s
ao caras para serem realizadas e difceis de serem implementadas corretamente.
De forma resumida, e f
acil e seguro realizar estas otimizacoes nas expressoes e difcil de serem
feitas no mundo das atribuicoes. Fica clara a vantagem das expressoes sobre as atribuicoes. O
prop
osito da programacao funcional e extender as vantagens das expressoes para as linguagens
de programacao.

1.4

Independ
encia da ordem de avalia
c
ao

Para entender as vantagens do mundo das express


oes e as fontes de suas propriedades, e necessario investigar a ordem de avaliacao nas express
oes aritmeticas. Avaliar alguma coisa signica encontrar seu valor. Assim, podemos avaliar a expressao aritmetica 5 4 + 3 encontrando
seu valor que, no caso, e 23.
Podemos tambem avaliar a expressao (3ax + b)(3ax + c)? A resposta e n
ao; a menos que
sejam conhecidos os valores de a, b, c e x. O valor desta expressao e dependente de um contexto
onde ela seja avaliada. Para isto, vamos avaliar esta express
ao em um contexto em que a = 2,
b = 3, c = 2 e x = 3. Esta avaliacao pode ser iniciada em v
arios pontos. Por motivo de
regularidade, ela ser
a feita da esquerda para a direita, lembrando, no entanto, que ela tambem
pode ser realizada da direita para a esquerda. Colocando todos os operadores de forma explcita,
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 a
que, neste contexto, e 2. Substituindo este valor na express
ao, ela se torna
(3 2 x + b) (3 a x + c)
Agora pode-se dar prosseguimento ao processo de avaliacao, substituindo a express
ao por
uma nova expressao:
(6 x + b (3 a x + c)
A seq
uencia completa de todos os passos realizados no processo de avaliacao e mostrada a
seguir, onde a exa dupla () signica 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


Figura 1.1: Arvore
representativa da express
ao (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 de
multiplicacao, , o resultado seria exatamente o mesmo, ou seja, qualquer ordem de avaliacao
produziria o mesmo resultado, 336. Isto ocorre porque, na avaliacao de uma expressao pura3 ,
a avaliacao de uma sub-expressao n
ao afeta o valor de qualquer outra sub-express
ao, uma vez
que n
ao existe qualquer dependencia entre elas. Na realidade, e possvel realizar as avaliacoes
f
destas sub-expressoes de forma concorrente. E
acil entender esta independencia da ordem de
avaliacao, utilizando uma arvore de avaliacao, conforme pode ser visualizado na Figura 1.1, onde
as vari
aveis cam nas folhas e as operacoes nos nos internos.
Nesta estrutura, cada operacao em um no depende apenas das operacoes dos nos abaixo
dele na arvore. A avaliacao de uma sub-
arvore afeta apenas a parte da arvore acima desta
sub-
arvore, ou seja, n
ao afeta as sub-arvores que estejam a sua esquerda ou a sua direita. O
processo de avaliacao e iniciado com a colocacao de alguns valores nas folhas. Os n
os internos sao
avaliados em qualquer ordem, sob demanda, podendo ate mesmo serem avaliados em paralelo.
Cada operacao depende apenas de suas entradas que s
ao os valores dos nos lhos. Este processo
de avaliacao pode ser visto gracamente na Figura 1.2.
A avaliacao de um n
o consiste na decoracao de cada n
o interno com o valor resultante
da avaliacao dos n
os abaixo dele. O processo de avaliacao termina quando a raiz da arvore for
decorada com o valor da express
ao completa. Isto pode ser visto na arvore da Figura 1.3.
3

Uma express
ao e dita pura quando n
ao realiza qualquer operaca
o de atribuica
o, explcita ou implcita.

15

Figura 1.3: Estado da arvore ap


os a avaliacao total da express
ao.
Como armado anteriormente, diversos processos podem acontecer em paralelo, na decoracao
da arvore, desde que seja observada a estrutura de arvore. Isto signica que sempre se chega ao
mesmo valor.
Esta propriedade vericada nas express
oes puras, ou seja, a independencia da ordem de avaliacao, e chamada de propriedade de Church-Rosser. Ela permite a construcao de compiladores
capazes de escolher a ordem de avaliacao que faca o melhor uso dos recursos da maquina. A
possibilidade de que a avaliacao seja realizada em paralelo, implica na possibilidade de utilizacao
de multiprocessadores de forma bastante natural.
Por outro lado, as express
oes impuras, normalmente, n
ao apresentam esta propriedade,
conforme pode ser vericado no exemplo a seguir, em Pascal. Seja a express
ao a+2*F(b). Ela
e pura ou impura? Para responder a isso, devemos vericar a denicao de F. Por exemplo,
function F(x : Integer) : Integer;
begin
F := x * x
end;
Como F n
ao executa qualquer atribuicao, a n
ao ser a pseudo-atribuicao a F para retornar o
valor da funcao, ela e uma funcao pura. Isto signica que, na avaliacao da expressao a+F(b),
pode-se avaliar a sub-express
ao a ou 2*F(b), que o resultado ser
a o mesmo. No entanto, vamos
supor que F fosse denida da seguinte forma:
function F(x : Integer) : Integer;
begin
a := a + 1;
F := x * x
end;
Neste caso, F e chamada de pseudo-func
ao, porque ela n
ao e uma funcao pura. Supondo
que a vari
avel a seja a mesma variavel da expressao a+2*F(b), como F altera o valor de a, o
valor 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 express
ao sera
ao tera o valor 2b2 + 1.
2b2 , ao passo que se 2*F(b) for avaliada primeiro, a express

1.5

Transpar
encia referencial

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

Figura 1.4: Grafo com um n


o compartilhado.
avaliador humano substituiria a sub-express
ao 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 xo sempre dara
como 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 expressaomostrada
na Figura 1.4. Como a sub-express
ao 3ax ocorre duas vezes, nao existe razao para se duplicar
a representacao na arvore. Neste caso, pode-se simplesmente rotear os dois arcos que usam a
sub-expressao para a mesma sub-
arvore.
A rigor, n
ao se tem mais uma estrutura de arvore, e sim um grafo acclico. No entanto,
pode-se decorar o grafo partindo das folhas, da mesma maneira feita antes. A u
nica diferenca
e que ap
os a decoracao do n
o compartilhado, seu valor pode ser usado por ambos os n
os acima
dele.
Esta propriedade e chamada de transparencia referencial, e signica que, em um contexto
xo, a substituicao de sub-express
oes por seus valores e completamente independente da expressao envolvente. Portanto, uma vez que uma express
ao tenha sido avaliada em um dado
contexto, n
ao e mais necessario avali
a-la novamente porque seu valor jamais sera alterado. De
forma mais geral, a transparencia referencial pode ser denida como a habilidade universal de
substituir iguais por iguais. Em um contexto em que a = 2 e x = 3, sempre pode-se substituir
3ax por 18 ou 18 por 3ax, sem que o valor da expressao envolvente seja alterado. A transparencia
referencial 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 import
ancia de poder substituir iguais por iguais. Isto conduz a` derivacao de novas equacoes, a
partir de equacoes dadas e a transformacao de expressoes em formas mais usuais e adequadas
para a prova de propriedades sobre elas.
No contexto das linguagens de programacao, a transparencia referencial permite otimizacoes
como a eliminacao de sub-express
oes comuns. Por exemplo, dada a segunda denicao da pseudofuncao F, da secao anterior, e claro que, como F deixa em a o registro do n
umero de vezes que
ela e chamada, n
ao se poderia eliminar a sub-expressao comum, F(b), da express
ao
(a+2*F(b))*(c+2*F(b))
17

Isto acontece porque a troca do n


umero de vezes que F e chamada altera o resultado da
expressao. Em algumas linguagens de programacao, isto complica a eliminacao de sub-express
oes
comuns.

1.6

Interfaces manifestas

A evolucao da notacao matematica ao longo dos anos tem permitido que ela seja utilizada
com sucesso, para exibir muitas propriedades. Uma destas propriedades se refere a`s interfaces
manifestas, ou seja, a`s conexoes de entradas e sadas entre uma sub-expressao e a expressao que
a envolve. Consideremos a express
ao 3 + 8. O resultado desta adicao depende apenas das
entradas 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 +. N
ao existem entradas escondidas para
este operador.
Vamos agora considerar o caso de uma funcao em uma linguagem de programacao convencional, onde o resultado a ser retornado pela funcao depende de uma ou mais vari
aveis locais
ou n
ao locais. Assim, chamadas sucessivas com as mesmas entradas podem produzir resultados
distintos. Por exemplo, seja a funcao f denida 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 vari
avel n
ao
local, a. O valor de a e atualizado em cada chamada a f, ent
ao f(3) tem valores diferentes em
cada chamada. Esta situacao caracteriza a existencia de interfaces escondidas para a funcao f,
tornando difcil, ou mesmo impossvel, se prever o comportamento da funcao.
Consideremos, novamente, a expressao (2ax + b) (2ax + c). Como pode ser observado, as
entradas para o primeiro operador + (2ax e b) estao manifestas. O papel da sub-expressao
2ax + b, dentro da express
ao completa, tambem e manifesto, ou seja, ela representa o argumento
esquerdo do operador de multiplicacao. N
ao existem sadas escondidas ou side eects na adicao.
Portanto, tanto as entradas como as sadas desse operador sao determinadas muito facilmente.
As entradas sao as sub-expressoes 2ax e b em qualquer lado do operador e a sada e liberada
para a express
ao envolvente.
Pode-se sumarizar as caractersticas das interfaces manifestas, da seguinte forma: as expressoes podem ser representadas por arvores e a mesma arvore representa tanto a estrutura
sint
atica de uma expressao quanto a forma como os dados uem na express
ao. As sub-expressoes
que se comunicam entre si podem sempre ser colocadas em posicoes contguas na arvore, como
na forma escrita da express
ao. Esta caracterstica nao e valida no mundo das atribuicoes, uma
vez que as variaveis alteraveis permitem comunicacao n
ao local. Em geral, o gr
aco do uxo
de dados n
ao e uma arvore e pode ser uma estrutura muito diferente da arvore sint
atica. Assim, pode n
ao ser possvel colocar juntas as partes comunicantes, de forma que suas interfaces
sejam obvias. A identidade estrutural das dependencias de dados e das dependencias sintaticas
e certamente uma das principais vantagens das express
oes puras.
18

1.7

Fun
c
oes e express
oes aplicativas

Vamos agora analisar as avaliacoes das expressoes aritmeticas. Estas expressoes sao estruturalmente simples, ou seja, s
ao construdas de forma uniforme pela aplicacao de operacoes
aritmeticas a seus argumentos. Mais importante que isto, estas operacoes sao funcoes puras, ou
seja, sao mapeamentos matematicos de entradas para sadas. Isto signica que o resultado de
uma operacao depende apenas de suas entradas. Alem disso, uma expressao construda a partir
de funcoes puras e constantes tem sempre o mesmo valor. Por exemplo, seja a funcao pura f
denida 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 denida em termos de funcoes
puras que s
ao as operacoes aritmeticas de adicao e multiplicacao. Vamos considerar agora, a
avaliacao de f (3ax) em um contexto em que a = 2 e x = 3. Como a avaliacao de expressoes
aritmeticas independe da ordem de avaliacao, assim tambem sera a avaliacao de f . Isto signica
que o argumento (3ax) de f pode ser avaliado antes ou depois da substituicao; o resultado ser
a
sempre o mesmo. Se ele for avaliado antes, a seq
uencia 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 ap
os a substituicao, a seq
uencia
de reducoes sera
f (3ax) (3ax + b) (3ax + c) (3 2 3 + b) (3ax + c) . . . 336
Estas duas seq
uencias de avaliacao correspondem aos metodos de passagem de parametro
por valor e por nome, respectivamente, nas linguagens de programacao comuns. A passagem
de par
ametros por valor, normalmente, e mais eciente porque o argumento e avaliado apenas
uma 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 escolhida n
ao ter inuencia no valor nal da express
ao, ela pode inuenciar sobre o termino, ou
n
ao, do processo de avaliacao. Por exemplo, a avaliacao da funcao f (x) 1 para x = 1/a em
um contexto em que a seja igual a zero tem diferenca nas duas ordens de avaliacao. Se 1/a for
avaliado antes da substituicao a avaliacao sera indenida e n
ao termina. Se for deixada para ser
feita depois da substituicao, o resultado ser
a 1. No primeiro caso, a avaliacao e dita estrita e
no segundo caso, ela e dita n
ao estrita em relacao a seu argumento x.
A programacao aplicativa, ou funcional, e freq
uentemente disting
uida da programacao imperativa que adota um estilo que faz uso de imperativos ou ordens. O mundo das atribuicoes e
caracterizado tambem por ordens, por exemplo, troque isto!, v
a para tal lugar!, substitua
isto! e assim por diante. Ao contrario, o mundo das express
oes envolve a descricao de valores,
por isto, o termo programac
ao orientada por valores.
Na programacao aplicativa, os programas tomam a forma de expressoes aplicativas. Uma
expressao deste tipo e uma constante (2, , e, etc) ou e composta totalmente de aplicacao de
funcoes puras a seus argumentos, que tambem sao expressoes aplicativas. Em BNF, as expressoes
aplicativas sao denidas da seguinte forma:
<EA> ::= <id> (<EA>, ...)
| <literal>
| <id>
A estrutura aplicativa se torna mais clara se as expressoes forem escritas na forma pre-xa,
ou seja, sum(prod (prod (2, a), x), b) em vez da forma usual 2ax+b.
19

A programacao aplicativa tem um u


nico construtor sint
atico que e a aplicacao de uma
funcao a seu argumento. Na realidade, este construtor e tao importante que, normalmente,
e representado de forma implcita, por justaposicao, em vez de explicitamente, atraves de algum
smbolo. Desta forma, sen x signica a aplicacao da funcao sen ao argumento x.
As vantagens da programacao sem atribuicoes em relacao a` programacao com atribuicoes sao
similares `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 resumidas 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 nveis mais altos de abstracao, atraves de mecanismos como funcoes de alta ordem, lazy evaluation e polimorsmo.
3. A programacao funcional representa um paradigma de programacao para a computacao
massivamente paralela, pela ausencia de atribuicoes, pela independencia da ordem de
avaliacao e pela habilidade de operar estruturas completas de dados.
4. A programacao funcional e uma aplicacao da Inteligencia Articial.
5. A programacao funcional e importante na criacao de especicacoes executaveis e na implementacao de prot
otipos com uma rigorosa fundamentacao matematica. Isto permite
vericar se as especicacoes estao corretas, ou nao.
6. A programacao funcional est
a fortemente acoplada a` teoria da computacao.

1.8

Defini
c
ao de fun
c
oes

A programacao funcional usando a linguagem Haskell e o principal objetivo desta Apostila. Isto
signica que devemos estudar formas de passagens das funcoes matematicas para funcoes codicadas em Haskell. Do ponto de vista matematico, as funcoes sao denidas sobre conjuntos,
domnio e imagem, mas nao se tem qualquer preocupacao sobre as formas como elas sao executadas para encontrar um valor resultado. J
a do ponto de vista da computacao, esta preocupacao
existe e e muito importante. No mundo da computacao, as funcoes sao declaradas sobre tipos
e leva-se em conta o algoritmo utilizado para implementa-las. A diferenca de desempenho de
um algoritmo em relacao a um outro, denido para executar a mesma funcao, tem importancia
fundamental no mundo da computacao. Apesar desta diferenca de pondo de vista entre estes
dois mundos, o processo de passagem de um para o outro e quase um processo de traducao
direta. Assim, neste Captulo, as funcoes serao denidas como na Matem
atica e deixamos a
an
alise do ponto de vista computacional para ser feita no Captulo 3.

Defini
c
oes de func
oes por enumerac
ao
Uma funcao e uma associacao de valores pertencentes a um conjunto de partida, o domnio, com
valores pertencentes a um conjunto de chegada, o contra-domnio ou imagem da funcao. Com
20

esta denicao em mente, uma funcao pode ser representada de duas maneiras. A primeira delas e
exibir todos os pares do tipo (entrada, sada), sendo esta uma denicao extensionista, tambem
conhecida como denicao por enumerac
ao, uma vez que todos os seus elementos sao exibidos. A
segunda maneira de representar uma funcao e exibir uma propriedade que apenas os elementos
desta funcao a tem. Isto signica a exibicao de uma regra que informa como cada elemento do
domnio e processado, transformando-o em um valor que e associado a um u
nico elemento da
imagem. Esta forma de apresentacao de uma funcao e conhecida como intencionista.
Por exemplo, a funcao booleana not tem como domnio e contra-domnio o conjunto {True,
False} e pode ser denida, por extens
ao ou enumeracao, da seguinte forma:
not True = False
not False = True
De forma similar, as funcoes or (disjuncao) e and (conjuncao) tambem podem ser denidas
por enumeracao, da seguinte maneira:
or
or
or
or

(False, False)
(False, True)
(True, False)
(True, True)

=
=
=
=

False
True
True
True

and
and
and
and

(False, False)
(False, True)
(True, False)
(True, True)

=
=
=
=

False
False
False
True

As denicoes de funcoes por intencionalidade ser


ao vistas a seguir, em mais detalhes, uma vez
que esta e a forma mais usual de apresentacao, dadas as v
arias formas como elas se apresentam.

Defini
c
ao de funco
es por composi
c
ao
Nao e difcil entender que as denicoes de funcoes por enumerac
ao so tem sentido se o domnio
e o contra-domnio forem nitos e de cardinalidade pequena. A grande maioria das funcoes nao
pode ser denida desta forma. Uma alternativa e denir as funcoes pela composic
ao de outras
funcoes ja denidas. Como exemplo, a funcao implica pode ser denida da seguinte forma:
implica (x, y) = or (not x, y)
Neste caso, e necessario saber como as funcoes a serem compostas sao aplicadas. A aplicacao
de uma funcao se torna simplesmente um processo de substituicao das funcoes primitivas. Por
exemplo, para avaliar a funcao implica (False, True) e necessario apenas fazer a substituicao
dos argumentos pelas aplicacoes das funcoes primitivas.
implica (False, True) = or (not False, True) = or (True, True) = True
Este processo e independente do domnio, ou seja, e independente de quando se est
a tratando
com funcoes sobre n
umeros, ou funcoes sobre caracteres, ou funcoes sobre arvores, ou qualquer
outro tipo. Em outras palavras, se a funcao f for denida por
f (x) = h(x, g(x))
entao sabemos que
f (u(a)) = h(u(a), g(u(a)))
independente das denicoes de g, h, u ou da constante a.
Frequentemente, a denicao de uma funcao n
ao pode ser expressa pela composicao simples
de outras funcoes, sendo necessaria a denicao da funcao para v
arios casos. Por exemplo, a
funcao que retorna o sinal algebrico de uma vari
avel pode ser denida da seguinte forma:
21

1,

se x > 0
0,
se
x=0
sinalg(x) =

1, se x < 0
De forma similar, a diferenca absoluta entre x e y pode ser denida da seguinte forma:


dif abs(x, y) =

x y, se x > y
y x, se x y

Defini
c
ao de funco
es por recurs
ao
Algumas situacoes existem em que e necessario denir uma funcao em termos de um n
umero
innito de composicoes. Por exemplo, a multiplicacao de n
umeros naturais () pode ser denida
por innitas aplicacoes da funcao de adicao (+). Sen
ao vejamos:

mn=

0,

n,

se
se
n + n,
se
n + n + n, se
..
.

m=0
m=1
m=2
m=3

Como nao e possvel escrever um n


umero innito de casos, este metodo de denicao de
funcoes so e usual se existir alguma regularidade ou algum princpio de unicac
ao entre
os casos, que permita gerar os casos nao denidos, a partir dos casos j
a denidos. Se existir
tal princpio de unicacao, ele deve ser estabelecido, sendo este o proposito das denic
oes
recursivas, onde um objeto e denido em termos de si pr
oprio. Por exemplo, uma denicao
recursiva da multiplicacao anterior pode ser:


mn=

0,
se m = 0
n + (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 b
asico de se fazer alguma operacao iterativamente.
Exerccios.
1. Avalie a express
ao 3 5, usando a denicao recursiva da multiplicacao mostrada nesta
secao.
2. Dena, recursivamente, em termos da multiplicacao denida nesta secao, a funcao pot
encia,
onde uma base e elevada a um expoente inteiro n
ao negativo.
3. Dena, recursivamente, a exponenciacao de n
umeros n
ao negativos elevados a uma potencia
inteira. Sugest
ao: use a denicao condicional e a resposta do exerccio anterior.
4. Dena, recursivamente, a adicao de inteiros n
ao negativos, em termos das funcoes sucessor
e predecessor.
5. Dena, recursivamente, a adicao de inteiros quaisquer.
6. Dena, recursivamente, a divisao por inteiros n
ao negativos, em termos das funcoes subtra
c
ao e menor que.
22

Defini
c
ao explcita de vari
aveis
Vamos considerar agora as denic
oes explcitas de funcoes, analisando inicialmente, as denicoes
explcitas de variaveis. Uma denicao de uma vari
avel e explcita se ela aparece no lado esquerdo
da equacao e n
ao aparece no lado direito. Por exemplo, a equacao
y = 2ax
dene explicitamente a variavel y. As denicoes explcitas tem a vantagem de que elas podem
ser interpretadas como regras de reescritas que nos informam como substituir uma classe de
expressoes por outra. Por exemplo, a denicao anterior de y, implica na regra de reescrita
y 2ax que nos diz como eliminar a vari
avel y em qualquer f
ormula, onde y ocorra. Por
2
exemplo, para eliminar y da expressao 3y + 5y + 1 aplica-se a regra de reescrita acima, para se
obter
3y 2 + 5y + 1 3(2ax)2 + 5(2ax) + 1
A nocao de denicao explcita de vari
aveis pode ser extendida aos conjuntos de equacoes
simultaneas. Um conjunto de vari
aveis e denido explicitamente por um conjunto de equacoes,
se
as operacoes forem individualmente explcitas e
elas puderem ser ordenadas, de forma que nenhuma delas use em seu lado direito uma
vari
avel j
a denida anteriormente na lista.
Por exemplo, o conjunto de equacoes

y =2ax

x=2

a=3

dene explicitamente y, x e a. As equacoes precedentes podem ser convertidas `as seguintes


regras de reescrita:

y 2ax

x2

a3

Estas regras podem ser aplicadas na seguinte ordem: a primeira delas pode ser aplicada ate
que n
ao exista mais y, em seguida a segunda e aplicada ate que n
ao exista mais x e, nalmente,
a terceira e aplicada ate que n
ao exista mais a. Desta forma teremos a seguinte seq
uencia de
reducoes:
y 2ax2a2 232

Defini
c
ao implcita de vari
aveis
Dizemos que uma vari
avel e denida implicitamente se ela for denida por uma equacao onde
ela aparece nos dois lados da equacao. Por exemplo,
2a = a + 3
dene implicitamente a como 3. Para encontrar o valor de a e necessario resolver a equacao
usando regras da algebra. O processo de solucao pode ser visto como uma forma de converter
uma denicao implcita em uma denicao explcita, mais usual, uma vez que uma denicao
implcita n
ao pode ser convertida diretamente em uma regra de reescrita. Assim, a equacao
23

2a = a + 3
n
ao nos informa explicitamente o que deve ser substitudo por a na expressao 2ax, por exemplo.
Alem disso, as regras de reescrita que resultam de denicoes explcitas sempre terminam, ou seja,
a aplicacao repetida das regras de reescritas elimina todas as ocorrencias da variavel denida.
Por outro lado, e possvel escrever denicoes implcitas que n
ao terminam, ou seja, nada
denem. Considere, como exemplo, a denicao implcita
a=a+1
Apesar de sabermos, claramente, que esta equacao n
ao tem solucao, este fato pode n
ao ser
t
ao obvio, em casos mais complexos. Se, ingenuamente, interpretarmos esta equacao como a
regra de reescrita
a a+1
ent
ao chegaremos a um nao determinismo na seguinte seq
uencia de reducoes:
2a 2(a + 1) 2((a + 1) + 1) . . .
As vari
aveis tambem podem ser denidas implicitamente por conjuntos de equacoes simult
aneas. Por exemplo, as equacoes
2a = a + 3
d 1 = 3d + a
denem implicitamente a = 3 e d = 2. Podemos tambem ter denicoes implcitas em que as
vari
aveis 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, denem implicitamente
a e x. Neste caso, nao existe qualquer forma de ordenacao destas equacoes de maneira que
as u
ltimas equacoes nao facam uso de vari
aveis ja denidas nas equacoes anteriores. A forma
implcita pode ser observada pela transformacao das duas equacoes em uma so, ou seja,
2a + 1 = a + 4
Em resumo, uma denicao explcita nos informa o que uma determinada coisa e, enquanto
uma denicao implcita estabelece algumas propriedades que esta coisa deve apresentar, exigindo que apenas esta coisa tenha estas propriedades. A determinacao do que esta coisa e
exige um processo de solucao.

Defini
c
oes explcitas e implcitas de func
oes
Ap
os termos analisado as denicoes explcitas e implcitas das variaveis, vamos agora considerar
como estas denicoes sao aplicadas ao caso das funcoes. Por exemplo, as duas equacoes, a seguir,
denem 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 implica (True, False). Usando a algebra booleana, estas equacoes podem ser resolvidas para se
chegar `a seguinte denicao explcita:
implica (p, q) = or (not p, q)
24

A denicao explcita permite que implica (True, False) seja avaliada usando substituicao.
Uma vantagem da programacao funcional e que, como a algebra elementar, ela simplica a
transformacao de uma denicao implcita em uma denicao explcita. Isto tem uma importancia
muito forte porque as especicacoes formais de sistemas de softwares, frequentemente, tem a
forma de denicoes implcitas, enquanto as denicoes explcitas s
ao, normalmente, f
aceis de
serem transformadas em programas. Assim, a programacao funcional prove uma forma de se
passar das especicacoes formais para programas satisfazendo estas especicacoes.
Exerccios.
1. Mostre que a denicao explcita da funcao implica (anterior) satisfaz a sua denicao
implcita.
2. Mostre que a denicao explcita da funcao implica e a u
nica solucao para a sua denicao
implcita, ou seja, nenhuma outra funcao booleana satisfaz estas duas equacoes, apesar de
que devem existir outras formas de expressar esta mesma funcao. Sugest
ao: usar Tabelaverdade.
Deve ser notado que as denicoes recursivas sao implcitas, por natureza. No entanto, como
seu 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 equacoes
seguintes constituem uma denicao recursiva de fatorial, para n 0.
fat n = n fat (n-1),
fat 0 = 1

se n > 0

elas podem ser convertidas `as seguintes regras de reescritas:


fat n n fat (n-1),
fat 0 1

se n > 0

Estas regras de reescritas nos dizem como transformar uma formula contendo fat. A realizacao destas transformacoes, no entanto, n
ao elimina, necessariamente, a funcao da f
ormula.
Por exemplo,
2 + f at 3 2 + 3 f at (3 1)
No entanto, se a computacao termina, ent
ao a aplicacao repetida das regras de reescrita
eliminar
a fat da f
ormula
2 + 3 f at 2 2 + 3 2 f at 1
2 + 3 2 1 f at 0
2+3211

1.9

Resumo

Este Captulo se relaciona com a fundamentacao teorica das linguagens funcionais. Esta fundamentacao e fortemente conectada com a Matematica e isto tem como vantagem primeiramente a
constatacao de essas linguagens tem um embasamenteo teorico sob o qual elas se fundamentam
e depois, sendo este embasamento oriundo da Matematica, torna estas linguagens mais trat
aveis
matematicamente e mais faceis de terem seus programas provados.
O objetivo deste Captulo foi mostrar a programacao funcional como uma programacao sem
atribuicoes, da mesma maneira que a programacao estruturada e uma programacao sem gotos.
Uma maquina, ao executar um programa estruturado, est
a, de fato, executando gotos, enquanto
25

uma maquina, ao executar um programa funcional, est


a, de fato, realizando atribuicoes. Em
ambos os casos, estas construcoes sao escondidas do programador, ou seja, elas estao em um
nvel inferior de abstracao.
A parte hist
orica mostrada no incio do Captulo foi baseada na Apostila de Rafael Lins
[22] e no livro de Brainerd [5]. A fundamentacao teorica foi, grande parte, baseada no livro de
Bruce Maclennan [24] que e um livro composto de duas partes, a primeira sendo pr
atica e a
segunda teorica. Ele adota esta metodologia de praticar e depois justicar a parte pr
atica. Esta
metodologia e inusitada e faz com que o livro seja uma referencia muito interessante n
ao apenas
pelo metodo utilizado mas, principalmente, pelo conte
udo muito bem escrito.
Outro livro fundamental neste estudo foi o livro de Antony Davie [7] que apresenta um
conte
udo pr
oximo da parte te
orica do livro de Maclennan. O Captulo tambem se baseia nos
ensinamentos do livro de Chris Okasaki [26] que apresenta uma teoria de linguagens funcionais
bastante profunda e objetiva.

26

Captulo 2

-c
alculo
Type theories in general date back
to the philosopher Bertrand Russell and beyond.
They were used in the early 1900s for the very specic
purpose 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

Introdu
c
ao

O -calculo foi desenvolvido por Alonzo Church no incio dos anos 30, como parte de um sistema
de l
ogica de ordem superior com o prop
osito de prover uma fundamentacao para a matematica,
dentro da losoa da escola logicista de Peano-Russel [22]. O -calculo e uma teoria que ressurge do conceito de funcao como uma regra que associa um argumento a um valor calculado
atraves de uma transformacao imposta pela denicao da funcao. Nesta concepcao, uma funcao
e representada por um grafo, onde cada n
o e um par (argumento, valor).
A l
ogica combinatorial foi inventada antes do -calculo, em cerca de 1920, por Moses
Sch
onnkel, com um prop
osito semelhante ao do -calculo [5]. No entanto, ela foi redescoberta 10 anos depois, em 1930, por Haskell B. Curry, que foi o maior respons
avel pelo seu
desenvolvimento ate cerca de 1970. Em 1935, Kleene e Rosser provaram que o -calculo e a
l
ogica combinatorial eram inconsistentes, o que provocou um desinteresse academico pelo tema.
No entanto, Curry n
ao desistiu de seu estudo e construiu uma extensao na l
ogica combinatorial
para ns de seus estudos, conseguindo sistemas mais fracos que a logica classica de segunda
ordem. Em 1975, Dana Scott e Peter Aczel mostraram que seu u
ltimo sistema apresentava
modelos interessantes [5]. Em 1941, Church desistiu de sua pesquisa inicial e apresentou uma
sub-teoria que lidava somente com a parte funcional. Essa sub-teoria, chamada de -calculo
foi mostrada ser consistente pelo teorema de Church-Rosser [22]. Usando o -calculo, Church
prop
os uma formalizacao da nocao de computabilidade efetiva pelo conceito de denibilidade
no -calculo. Kleene mostrou que ser denvel no -calculo e equivalente a` recursividade de
G
odel-Herbrand. Concomitantemente, Church formulou a sua conjectura associando a recursividade como a formalizacao adequada do conceito de computabilidade efetiva. Em 1936, Allan
Turing modelou a computacao automatica e mostrou que a nocao resultante (computabilidade
de Turing) e equivalente a` denibilidade no -calculo. Desta forma, o -calculo pode ser visto
como uma linguagem de programacao. Isto implica que podem-se analisar diversos tipos de
problemas de programacao, em particular, os relacionados com chamadas a procedimentos [22].
27

Curry e seus colegas [6], Barendregt [3] e outros desenvolveram extensivamente os aspectos
sint
aticos 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 usu
arios futuros do
-calculo.

2.2

-express
oes

O -calculo tem apenas quatro construcoes: vari


aveis, constantes, aplicacoes e abstracoes. Suponhamos que sejam dadas uma seq
uencia innita de smbolos distintos chamados de vari
aveis e
uma seq
uencia nita, innita ou vazia de smbolos distintos, chamados constantes [29]. Quando
a seq
uencia de constantes for vazia, o sistema e chamado de puro; em caso contrario, e chamado
de aplicado. O conjunto de express
oes, chamadas de -expressoes, e denido indutivamente da
seguinte forma:
todas as vari
aveis e constantes sao -expressoes (chamadas de
atomos);
se M e N forem -expressoes, entao (M N ) tambem e uma -expressao, chamada combinac
ao ou aplicac
ao;
se M for uma -expressao e x for uma vari
avel qualquer, ent
ao (x.M) tambem e uma
-expressao, chamada abstrac
ao ou func
ao.
As -expressoes, assim denidas, podem ser formuladas utilizando a notacao BNF. Isto e
feito da seguinte maneira, onde elas sao representadas por < exp >:
< exp > ::

< constante >


|< variavel >
| (< exp >< exp >)
| ( < variavel > . < exp >)

constantes embutidas
nomes de variaveis
combinacao ou aplicacao
abstracao ou funcao.

Apesar das denicoes acima serem claras, elas sao muito rgidas e, am alguns casos, podem
surgir d
uvidas sobre as regras de escopo, ou seja, ate que ponto em uma -expressao uma vari
avel
tem inuencia. Assim, as observacoes a seguir devem ser utilizadas para dirimir d
uvidas, por
acaso existentes.
Observa
c
oes importantes:
1. As vari
aveis sao representadas por letras romanas min
usculas.
2. As -expressoes completas sao representadas por letras romanas mai
usculas ou por letras
gregas min
usculas (exceto , , e que tem signicados especiais) ou por letras gregas
mai
usculas.
3. Apesar da rigidez mostrada na BNF das -expressoes, os parenteses devem ser usados apenas para resolver ambig
uidades [38]. Esta e uma convencao que diminui muito
o tamanho das -expressoes e deve ser utilizada, sempre que possvel. Por exemplo,
(x.(y.(. . . (z.(E)) . . .))) pode e deve ser escrita como xy . . . z.E, signicando que grupos de abstracoes ou funcoes sao associados pela direita. No entanto, grupos de combinacoes ou aplicacoes termos combinados sao associados pela esquerda, ou seja, E1 E2 E3 . . . En
signica (. . . ((E1 E2 )E3 ) . . . En ). Alem disso, a.CD representa (a.CD) e n
ao (a.C)(D),
estabelecendo que o escopo de uma vari
avel se extende ate o primeiro parentese descasado
encontrado a partir da vari
avel, ou atingir o nal da -expressao.
4. A escolha de constantes, de funcoes embutidas para serem utilizadas na manipulacao destas
constantes e/ou n
umeros e das funcoes para o processamento de listas e arbitr
aria.
28

Estas observacoes sao importantes porque elas podem ser utilizadas para dirimir algumas
d
uvidas que, com certeza, v
ao existir, principalmente por quem esta travando um primeiro
comum n
contacto com o processo de avaliacao no -calculo. E
ao se reconhecer imediatamente
o escopo de uma vari
avel e estas observacoes podem ser um meio importante para dirimir estas
d
uvidas.
Exemplos
S
ao 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 -c
alculo

Nesta secao sera mostrada a sintaxe do -calculo, cando a semantica para uma pr
oxima.
Todas as aplicacoes de funcoes sao pre-xas, signicando que, por exemplo, a express
ao
(+( 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 uma
expressao que vai ser avaliada.
Esta avaliacao se da pela selecao repetida de express
oes redutveis, conhecidas como redexes, 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 n
ao representar um redex porque
seus parametros ainda n
ao estao todos avaliados. A avaliacao da -expressao acima e feita
com a seguinte seq
uencia de reducoes:
(+( 2 3) ( 8 2)) (+ 6( 8 2)) (+ 6 16) 22

2.3.1

Aplica
c
ao de func
ao e currificac
ao

A aplicacao de uma funcao f a um par


ametro 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) e
interpretada 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 de
uma funcao seja tambem uma outra funcao. Este resultado foi descoberto por Sch
onnkel, mas
foi amplamente utilizado por Curry e, por este motivo, passou a ser conhecido como curricac
ao.
Dito de outra forma, curricac
ao e uma caracterstica muito importante de algumas linguagens, onde uma funcao com n argumentos pode ser interpretada como n funcoes de apenas 1
(um) argumento. Isto signica que, para o -calculo, todas as funcoes tem apenas um argumento.
29

2.4

Fun
c
oes e constantes pr
e-definidas

Uma caracterstica importante do -calculo e a facilidade com que as funcoes podem ser construdas pelo usu
ario. No entanto, algumas delas juntamente com algumas constantes ja foram
pre-denidas e que podem ser utilizadas pelos usu
arios. 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) onde
HEAD (CONS a b) a
TAIL (CONS a b) b
Exemplos:
a) 5 4 1
b) AND TRUE FALSE FALSE

2.5

-abstrac
oes

As funcoes embutidas no -calculo s


ao formalizadas como ja mostrado anteriormente. No entanto, deve existir uma forma de construir funcoes que nao sejam embutidas. Isto e feito atraves
de um construtor (). Uma -abstracao e um tipo particular de express
ao que denota uma
funcao. Por exemplo,(x . + x 1) e uma -abstracao que deve ser interpretada da seguinte
forma:

indica que se trata de uma funcao


x
sobre a variavel x
.
que
+ x 1 adiciona x ao n
umero 1.
Uma -abstracao tem sempre estes 4 (quatro) elementos: o construtor (), o par
ametro
formal (uma vari
avel, no caso, x) o ponto (.) e o corpo da funcao (+ x 1). Uma -abstracao pode
ser comparada com as funcoes em uma linguagem de programacao imperativa. Por exemplo, a
-abstracao anterior pode ser representada em C pelo seguinte fragmento de c
odigo:
int inc (x)
int x;
{ return (x + 1)

};

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

2.6

A sem
antica operacional do -c
alculo

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 mostradas estas regras, devemos denir alguns termos que sao utilizados na aplicacao destas regras.
Uma ideia central no estudo das linguagens de programacao, da notacao matematica e da
l
ogica simbolica e a de ocorrencia livre e ocorrencia ligada (ou conectada) de uma vari
avel. Esta
ideia, normalmente, provoca confusao em quem esta vendo o tema pela primeira vez. Assim, ele
sera introduzido de forma gradual e intuitiva, utilizando exemplos j
a conhecidos da Matematica.
Assim, vamos considerar o somat
orio:
n


i2 + 1

i=1

Nesta expressao, a vari


avel i e uma vari
avel ligada, ou seja, ela esta fortemente atrelada
ao somatorio. Diz-se que ela ocorre ligada nesta expresssao. Uma das caractersticas inerentes
`as vari
aveis ligadas e que elas podem ser renomeadas sem que o signicado da expressao seja
alterado. Por exemplo, o mesmo somatorio anterior pode tambem ser representado da seguinte
forma, sem haver qualquer modicacao em seu signicado:
n


k2 + 1

k=1

De forma identica, a integral


 1

x2 3xdx

com respeito a x, representa a mesma integral


 1
0

t2 3tdt

agora relacionada a vari


avel t. Na teoria dos conjuntos, o conjunto de todos x tal que x 0 e o
mesmo 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 express
ao, tambem envolvendo somat
orio:
i

n


(j 2 + i a)

j=1

Nesta expressao, a ocorrencia da vari


avel j e ligada. Isto pode ser vericado pelo fato de
que ela pode ser trocada por qualquer outra vari
avel, desde que n
ao seja i ou a, sem mudar em
nada o signicado da express
ao. Por exemplo, a expressao
31

n


(k2 + i a)

k=1

tem, exatamente, o mesmo signicado da anterior. Em uma expressao, uma ocorrencia nao
ligada de uma vari
avel e dita ser livre. As vari
aveis i, a e n ocorrem livre nesta expressao.
Se uma ocorrencia de uma vari
avel livre for trocada, o signicado da express
ao tambem sera
trocado. Alem disso, uma ocorrencia de uma vari
avel pode ser ligada em uma expressao e livre
em outra. Por exemplo, a vari
avel i e livre em
n


j2 + i a

j=1

e em

n


ai2 j 2

j=1

mas e ligada na expressao


m

i=1

i!

n


j 2 + i a

j=1

O local da vinculacao (binding site) de um identicador determina seu escopo, que e a regiao
da expressao na qual o identicador ocorre ligado. Esta regi
ao, normamente, e indicada por
alguma convencao lexica, como os parenteses, colchetes ou chaves. Ainda nesta secao, sera visto
que esta regiao e o corpo da expressao.
Como visto, a troca de vari
aveis ligadas nao interfere no signicado da express
ao. No entanto,
esta troca nao pode ser feita para uma vari
avel que ocorra livre na express
ao, caso contrario a
expressao muda seu signicado. Por exemplo, a express
ao que representa a soma dos elementos
da linha i de uma matriz Amn e o somatorio
n


Aij

j=1

A vari
avel j ocorre ligada nesta expressao e, neste caso, pode ser trocada por outra, por
exemplo, k.
n


Aik

k=1

cujo signicado e o mesmo da expressao anterior. No entanto, observemos que a vari


avel j n
ao
pode ser trocada pela vari
avel i. Neste caso, a expressao se tornaria o somatorio de Aii , que
representa o somatorio dos elementos da diagonal da matriz e n
ao mais dos elementos da linha
i, como inicialmente. Este fenomeno e conhecido como colis
ao de identicadores e deve ser
evitado.
Fazendo um paralelo com as linguagens de programacao comuns, as vari
aveis ligadas das
expressoes correspondem exatamente aos parametros formais das funcoes. Na Matem
atica, a
funcao f (x) x2 3x tem x como variavel ligada e como seu parametro formal.
32

2.6.1

Formaliza
c
ao das ocorr
encias livres ou ligadas

Ap
os termos visto as nocoes de ocorrencias ligada e livre, como tambem a nocao de colisao de
identicadores 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 sobre
a vari
avel x, onde o corpo desta funcao e (+ x y).
A vari
avel x pode ser pensada como um local onde o argumento 4 deve ser colocado. J
a
para a vari
avel y, este mesmo raciocnio n
ao pode ser aplicado, uma vez que nao existe este
local sinalisado pela variavel y seguida ap
os um . Isto implica que as duas vari
aveis (x e y)
tem status distintos.
Formalmente, dene-se: uma ocorrencia de uma vari
avel e ligada se existir uma -abstrac
ao
` qual esta vari
a
avel esteja ligada. Em caso contr
ario, a ocorrencia e livre. A Figura 2.1 mostra
um exemplo detalhado de vari
aveis 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) ligada(s). Por exemplo, na -expressao + x ((x . + x 1) 4), a primeira ocorrencia de x e livre e
a segunda e ligada ou conectada.
Podemos dizer, sendo x e y duas vari
aveis e e duas -expressoes, que:
a) x e livre em y
b) x e livre em y .
c) x e livre em

se x = y. Se x
= y, a ocorrencia de x e ligada.
se (x for livre em ) E (x
= y)
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. N
ao existem variaveis livres nos combinadores I, K, S, , Y, (ver secao 2.7).

2.7

Combinadores

Uma -expressao que n


ao apresenta vari
aveis livres e chamada fechada ou combinador. Existem
alguns combinadores que desempenham um papel especial no estudo do -calculo, conforme foi
armado na Introducao desta Apostila, onde foi feita referencia aos combinadores S, K e I, nos
quais e baseada a m
aquina de reduc
ao-SK de Turner. Podemos tambem citar:
I = x.x
K = x.y.x
S = x.y.z.xz(yz)
= x.xx
Y = f.(y.f (yy))(y.f (yy))
= a.b.b(aab)

Identidade
Projecao
Composicao
Duplicacao
Usado na representacao de funcoes recursivas
Tambem usado na representacao de funcoes recursivas
33

Tabela 2.1: Resumo


Uma ocorrencia de uma vari
avel deve ser livre ou ligada.
Denicao de ocorrencia li- x ocorre livre em x
vre:
x ocorre livre em (E F ) x ocorre livre em E ou
x ocorre livre em F
x ocorre livre em y . E x e y sao vari
aveis distintas e
x ocorre livre em E
Denicao de ocorrencia ligada:

x ocorre ligada em (E F ) x ocorre ligada em E ou


x ocorre ligada em F
x ocorre ligada em y . E (x e y sao a mesma variavel e
x ocorre livre em E) ou
x ocorre ligada em E.

Exerccios resolvidos
1. Identique nas express
oes abaixo aquelas que s
ao, ou n
ao, -expressoes:
a) a
Sim, a e uma vari
avel;
b) 9
Sim, 9 e uma constante;
c) ((b.b)(a.ab)
N
ao, os parenteses nao estao aninhados
corretamente;
d) (x.)a
Nao, x. n
ao e um termo;
e) ((x.y.y)(y.yyy))((i.i)(a.b)) Sim, porque contem apenas aplicacoes,
abstracoes e variaveis.
2. Identique nas express
oes abaixo as ocorrencias livres e as ligadas das variaveis
a) x.xx
As duas ocorrencias da variavel x sao conectadas
b) (x.y.x)x
Ou
ltimo x ocorre livre
c) (x.y.xx)xa As duas u
ltimas vari
aveis ocorrem livres
d) (x(x.y))x
Todas as variaveis ocorrem livres
Exerccios propostos
1. Justique porque as express
oes abaixo n
ao sao -expressoes:
(a) (x(y)z)
(b) (x.y.x((i.ii)(b.c.b)(b.c.b))
(c)
(d) x.x
2. Identique 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

(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 convers
oes entre -express
oes

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 n
ao pode mais ser transformada em outra mais simples, ou entrar em loop
innito. Esta secao e devotada a` an
alise das tecnicas que sao utilizadas para promover estas
transformacoes. Na realidade elas tem um papel similar ao que as tecnicas algebricas tem na
solucao de equacoes da Matem
atica.
Existem tres tecnicas basicas utilizadas na conversao de uma -expressao em outra. S
ao
elas: a -conversao, a -conversao e a -conversao.

2.8.1

-convers
ao

Ao analisarmos as duas -expressoes (x .+ x 1) e (y . + y 1), vericamos que elas apresentam


o mesmo comportamento, ou seja, a u
nica diferenca entre elas esta nos nomes dos par
ametros,
signicando que elas representam a mesma -expressao. Deduz-se, portanto, que estas duas
-expressoes sao convertveis uma na outra. Esta convers
ao e chamada de -conversao que nos
permite trocar os nomes dos par
ametros formais (vari
aveis ligadas) de qualquer -abstracao.
Assim,

(x . x 1) (y. y 1)

2.8.2

-convers
ao

Analisemos, agora, estas duas -abstracoes: (x . + 1 x) e (+ 1). Vericamos que elas se


comportam exatamente da mesma maneira quando aplicadas a um argumento. Assim, elas
tambem sao convertveis uma na outra, ou seja,

(x . + 1 x) (+ 1) ou, mais formalmente,

ao ocorra livre em F e F seja uma funcao.


(x . F x) F , desde que x n
Exemplos:
1. a -expressao (x . + xx) n
ao e -redutvel a (+ x) porque a vari
avel x ocorre livre em
(+ x).
2. (x . T RU E x) n
ao e -redutvel a T RU E porque T RU E n
ao e uma funcao.

2.8.3

-convers
ao

O resultado da aplicacao de uma -abstracao a um argumento e uma inst


ancia do corpo da
-abstracao na qual as ocorrencias livres do parametro formal no corpo da -abstracao sao
trocadas por c
opias do argumento.
35

Exemplo: a aplicacao da -abstracao (x . + x 1) 4 reduz-se para + 4 1 que e uma inst


ancia
do corpo + x 1, onde trocamos a ocorrencia livre de x pelo argumento 4. Esta operacao e
chamada 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.
Observa
c
oes:
1. Nas reducoes acima, deve ser observada a curricacao 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 da
mesma forma. Por exemplo,

(f . f 3)(x . + x 1) (x . + x 1)3 + 3 1 = 4

2.8.4

Nomea
c
ao

Deve ser tomado algum cuidado na escolha dos nomes dos parametros, uma vez que mais de
uma vari
avel 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)3
que e igual a + 8 3 que, nalmente, e igual a 11.
Deve ser observado que o x interno de ( x 1) da primeira linha deste exemplo n
ao foi
substitudo, uma vez que ele n
ao ocorre livre em (x . + ( x 1)) que e o corpo da -abstracao
mais externa.

2.8.5

Captura

O uso de nomes de vari


aveis pode algumas vezes criar situacoes confusas envolvendo -reducoes.
Por exemplo, seja a -expressao (x . (y . yx))y. O y mais externo desta -expressao ocorre
livre. Se executarmos uma -reducao vamos obter a -expressao y . yy. Esta -expressao resultante apresenta um signicado distinto da anterior porque o y externo, que era livre, tornou-se
conectado. Esta situacao e conhecida como o problema de captura de vari
aveis. A possibilidade
de captura causa complicacoes para a avaliacao mecanica de -expressoes. A solucao trivial para
este problema e efetuarem-se -conversoes antes de se realizar a -conversao para se evitar que
vari
aveis distintas, mas homonimas, sejam confundidas. No caso da -expressao citada, uma
seq
uencia correta de reducoes seria:

(x . (y . yx))y (x . (z . zx))y z . zy
O mesmo problema pode ser vericado na -expressao (x . z . xz)(y . yz) que seria redutvel a (z . (y . yz)z) e o z de (y . yz) perderia seu contexto original, sendo capturado pelo
36

z de (x . z . xz). Neste caso, uma possvel seq


uencia 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, como
C e Pascal. Suponhamos que na declaracao de um procedimento se utilize uma vari
avel local
denominada x e que na execucao deste mesmo procedimento ele receba como parametro real
uma vari
avel tambem denominada x que foi declarada fora do procedimento, portanto n
ao local,
em relacao a ele. Uma referencia `a vari
avel n
ao local x sera feita, na realidade, a` vari
avel x
local, ou seja a variavel x externa foi capturada pela vari
avel x interna. Isto acontece porque as
vari
aveis tem o mesmo nome (sao hom
onimas), apesar de representarem entidades distintas.
Exemplos resolvidos.
1. Faca a seq
uencia 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)
=+53
=8
2. Deve ser vericado que as funcoes embutidas podem ser construdas como quaisquer outras
funcoes. Por exemplo,
CON S = (a . b . f . f a b),
HEAD = (c . c(a . b . a)) e
T AIL = (c . c(a . b . b)).
Vejamos agora uma seq
uencia 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 h
a a necessidade de que os construtores HEAD, CONS, TAIL ou uma
funcao qualquer sejam pre-denidas. Na realidade, eles existem apenas por quest
ao de eciencia.

2.9

Convers
ao, redu
c
ao e abstrac
ao

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
ametro de forma consis1. Troca de nomes: -conversao permite trocar o nome de um par
tente.
37

2. Aplica
c
ao de fun
c
ao: -conversao permite aplicar uma -abstracao a um argumento,
construindo uma nova inst
ancia do corpo da -abstracao.
3. Elimina
c
ao de -abstra
c
oes redundantes: -reducao pode, algumas vezes, eliminar
-abstracoes redundantes.

2.10

Provando a conversibilidade

Muito freq
uentemente, nos deparamos com casos nos quais temos que provar a conversibilidade
entre duas -abstracoes. Quando as duas -expressoes denotam a mesma funcao, o mecanismo
de prova pode se tornar muito complicado e tedioso. Por exemplo, sejam as -expressoes IF
TRUE ((p . p) 3) e (x . 3). Ambas denotam a mesma funcao, ou seja, a funcao que sempre
retorna o valor 3, independente dos valores dos argumentos reais. Assim, espera-se que elas
sejam conversveis uma na outra, j
a que denotam a mesma funcao. Realizando as -conversoes
sobre a primeira, temos:
IF TRUE ((p . p) 3)

IF TRUE 3

(x . IF TRUE 3 x)
= (x . 3)

- pela denicao de IF.

Um metodo alternativo de se provar a convertibilidade de duas -expressoes que denotam a


mesma funcao, e que normalmente e mais conveniente, consiste em aplicar ambas as -expressoes
a um mesmo argumento arbitrario, por exemplo, w. Vejamos como ca a prova de convertibilidade das duas funcoes anteriores.
Tabela 2.2: Exemplo de aplicacoes ao mesmo argumento.
IF TRUE ((p . p) 3) w
(p . p) 3

(x . 3) w
pela def de IF

Portanto, IF T RU E ((p . p)3) (x . 3)


Este esquema de prova tem a vantagem de usar apenas reducao e evitar o uso explcito de
-reducoes.

2.11

Uma nova nota


c
ao

A aplicacao das regras de conversao nem sempre e tao simples e direta e, por isso mesmo,
sera mostrada uma denicao formal do que exatamente elas representam. Para isto, vamos
introduzir uma nova notacao que e bastante utilizada em exerccios de conversao de redexes, por
ser intuitiva para a implementacao de redutores computacionalmente.
A notacao E[M/x] signica que, na express
ao E, todas as ocorrencias livres de x serao
substitudas 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

Tabela 2.3: Resumo das conversoes.


x[M/x] = M
c[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 vari
avel distinta de x
= y . E[M/x], se x n
ao ocorre livre em E OU y n
ao ocorre livre em M
= z . (E[z/y]) [M/x], onde z e o nome de uma nova vari
avel que n
ao
ocorre livre em E ou em M.
Denicoes:

-conversao: se y n
ao e livre em E, entao (x . E) (y . E[y/x])

-conversao: (x . E) M E[M/x]

-conversao: se x n
ao e livre em E e E denota uma funcao, ent
ao (x . E x) E

2.12

Ordem de reduc
ao

Um redex (reduction ex pression) e uma -expressao na qual todos os par


ametros necessarios
para que uma operacao possa ser feita estao prontos para serem utilizados. Se uma expressao
n
ao contiver qualquer redex a sua avaliacao esta completa e, neste caso, diz-se que a expressao
esta na sua forma normal.
No entanto, pode acontecer que uma express
ao tenha mais de um textitredex e, neste caso,
teramos mais de um caminho a ser seguido para percorrer a seq
uencia de avaliacoes. Vericamos
que, utilizando-se qualquer uma das seq
uencias de reducao, o resultado seria o mesmo. Por
exemplo, + (* 3 4) (* 7 8) apresenta dois redexes. Iniciando a avaliacao da esquerda para a
direita temos a seguinte seq
uencia de avaliacao:
+ (* 3 4) (* 7 8)
+ 12 (* 7 8)
+ 12 56
68
Se, no entanto, zermos a avaliacao da direita para a esquerda, seq
uencia de reducao sera:
+ (* 3 4) (* 7 8)
+ (* 3 4) 56
+ 12 56
68
Vericamos que o resultado das duas seq
uencias de avaliacao e exatamente o mesmo. No
entanto, algumas observacoes devem ser feitas:
1. nem toda -expressao tem uma forma normal. Por exemplo, a -expressao (), onde
= (x . x x) tem a seguinte seq
uencia de reducoes:

(x . x x) (x . x x) (x . x x) (x . x x) (x . x x) (x . x x) . . .
correspondendo a um loop innito nas linguagens imperativas.
2. Algumas seq
uencias de reducao podem atingir a sua forma normal enquanto outras n
ao.
Por exemplo, (x . 3) () pode ser avaliada para 3, usando-se o primeiro redex, mas, se
escolhermos o segundo ( aplicado a ), entraremos em loop innito.
39

No entanto, ao perguntarmos se seq


uencias diferentes de reducao podem levar a formas

normais tambem diferentes, a resposta e NAO.


Esta resposta incisiva e justicada por dois
teoremas que serao descritos a seguir, apesar de n
ao exibirmos suas demonstracoes, que sao
deixadas para consulta por alguem mais interessado e curioso sobre o tema, dado o carater
introdut
orio desta Apostila, e estas demonstracoes requerem conhecimentos avancados sobre a
teoria da computacao.
ao existe uma expressao E tal
Teorema 1 de Churh-Rosser (CRT-I).Se E1 E2 , ent
que E1 E e E2 E.
ao pode ser convertida a duas formas normais distintas.
Corol
ario. Nenhuma express
Dito de uma maneira informal, todas as seq
uencias de reducoes que terminam, chegarao ao
mesmo resultado, ou seja, a forma normal, se existir, e u
nica.
O teorema de Church-Rosser e conhecido como teorema da atingibilidade e unicidade da
forma normal e tem uma longa hist
oria: ele foi primeiramente demonstrado por Alonzo Church
e J. Barkley Rosser em 1936 [24]. Esta demonstracao era tao longa e complicada que muito do
trabalho de pesquisa posterior foi feito tentando descobrir formas mais simples de demonstracao
deste teorema. Assim, o teorema ja foi demonstrado de v
arias formas para diferentes prop
ositos,
mas, todas elas apresentam alguma diculdade. W. Tait, P. Martin-L
of e outros pesquisadores iniciaram um sistema de demonstracao do teorema a partir de 1972. Uma demonstracao
mais simples foi apresentada pelo prorio J. Barkley Rosser em 1982, tambem apresentada em
MacLennan [24], e uma vers
ao 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 relac
ao R tem a propriedade do
diamante se e somente se, para todas as f
ormulas bem formadas X, X1 e X2 vale: se X R X1
e X R X2 , ent
ao existe uma f
ormula 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 seguinte
forma: a redu
c
ao tem a propriedade do diamante.
Teorema 2 de Church-Rosser (CRT II). Se E1 E2 e E2 esta na forma normal,
ent
ao existe uma ordem normal de seq
uencias de reducao de E1 para E2 .
Este teorema e tambem conhecido como teorema da normalizac
ao e signica que existe,
no m
aximo, um resultado e a ordem normal o encontra, se ele existir. A ordem normal de
reducao especica que o redex mais `a esquerda e mais externo - conhecido na literatura como
leftmost-outermost - deve ser realizado primeiramente. Dito de outra forma, a redu
c
ao do
redex mais externo e mais a
` esquerda, em cada ponto da seq
u
encia de redu
c
oes,
nos leva at
e a forma normal, se ela existir .
Exemplo
Utilize a ordem normal de reducao para determinar a forma normal da express
ao (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

Fun
c
oes recursivas

A ideia de escolher o -calculo como linguagem intermedi


aria entre as linguagens funcionais,
de alto nvel, e a linguagem de m
aquina signica que todas as funcoes devem ser traduzidas
40

para ele. Na realidade, os programas funcionais representam apenas uma forma mais adequada
de -expressoes. Dizemos que os programas funcionais sao acucaramentos sintaticos de expressoes do -calculo, para torn
a-lo mais adequado e mais f
acil de ser tratado.
No entanto existe um problema a ser resolvido que se refere ao fato de uma das caractersticas
not
aveis dos programas funcionais e a utilizacao massiva de funcoes recursivas [29] e estas funcoes
n
ao tem correspondentes no -calculo, conforme j
a foi visto ate aqui. Isto acontece porque, no
-calculo, as funcoes sao an
onimas e, portanto, n
ao 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 necessidade de qualquer extens
ao.
Voltemos, momentaneamente, nossa atencao para a denicao matematica da funcao f (x) =
Na realidade, estamos procurando valores para x e os respectivos resultados da aplicacao
da funcao f a estes valores. Como um caso particular, vamos procurar valores x que satisfacam
a igualdade f(x) = x, ou seja, estaremos procurando valores de x para os quais x3 x = x. Um
tal valor e x = 0, porque f (0) = 0. Mas x = 21/2 tambem sao outros valores que satisfazem a
igualdade f(x) = x.

x3 x.

Estes valores sao chamados de pontos xos e representam uma grande fonte de estudo matematico, destacando-se o teorema do ponto xo, alem de outros. Os pontos xos apresentam
caractersticas importantes, no entanto o nosso interesse aqui se prende exclusivamente em utiliz
a-los na construcao de funcoes recursivas no -calculo.
Considere agora a seguinte denicao recursiva da funcao fatorial:
FAT = (n . IF (= n 0) 1 (* n (FAT (- n 1)))).
Nesta denicao, damos um nome a uma -abstracao (FAT ) e nos referimos a ele mesmo,
dentro da -abstracao. Este tipo de construtor n
ao e provido pelo -calculo porque as abstracoes sao funcoes anonimas e, portanto, elas n
ao podem fazer referencias a nomes.
Vamos colocar a -expressao F AT em uma forma mais adequada ao nosso desenvolvimento.
Teremos entao FAT = n . (. . .FAT. . .), onde os pontos representam as outras partes da funcao que n
ao 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 denicao de H e adequada aos nossos prop
ositos, uma vez que ela e uma -abstracao
ordin
aria e n
ao usa recursao. A equacao FAT = H FAT estabelece que quando a funcao H e
aplicada a FAT o resultado e o pr
oprio FAT. Ent
ao, FAT e um ponto xo de H.
Vamos agora procurar um ponto xo para H. Para isto vamos criar uma funcao, Y , que
toma H como argumento e retorna um ponto xo da funcao, como resultado. Assim Y deve ser
tal que Y H seja um ponto xo de H. Portanto, H(YH) = YH. Por este motivo, Y e chamado
de combinador de ponto xo. Se formos capazes de produzir tal combinador, nosso problema
estara resolvido.
Agora denimos FAT = Y H. Esta denicao n
ao e recursiva e atende a`s nossas necessidades.
Para vericar isto, vamos computar (FAT 1) utilizando as denicoes de FAT e de H, dadas
anteriormente e que o mecanismo de calculo obedece `a ordem normal de reducao ou seja, leftmostoutermost.
FAT = Y H
H = fat . n . IF (= n 0) 1 (* n(fat (- n 1))).
Ent
ao
41

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 denicao de IF
= (* 1 ((H (Y H)) (- 1 1)))
pelo fato de YH ser um ponto xo 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 denicao de IF
1
A forma como o combinador Y e denido j
a foi mostrada anteriormente, no entanto ele ser
a
novamente aqui denido, usando apenas algumas renomeacoes de vari
aveis:
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 conrma o fato de que Y H e um ponto xo de H ou seja, o combinador Y ,
quando aplicado a uma funcao, retorna um ponto xo desta funcao.

2.14

Resumo

Este Captulo versou sobre os primeiros passos para quem necessita conhecer o -calculo de
forma simples e introdut
oria. Ele se fez necessario dada a import
ancia que esta teoria tem
na fundamentacao das linguagens funcionais. Seu papel e semelhante ao desempenhado pela
linguagem Assembly na traducao das linguagens imperativas para o c
odigo de maquina. No
entanto, deve ser salientado que esta teoria da matematica n
ao e tao simples como aqui parece.
Esta abordagem simples foi adotada, dado o car
ater introdut
orio exigido para se entender como
o -calculo e utilizado na compilacao de linguagens funcionais. Quem quizer se aprofundar neste
tema deve consultar a bibliograa indicada.
As notas de aula de Rafael Lins [22] e de Peter Welch [38] representam um bom comeco,
dada a grande quantidade de exerccios indicados e resolvidos. Quem estiver interessado em
detalhes mais aprofundados sobre a implementacao do -calculo deve consultar o livro de Peyton
Jones [29] que apresenta o -calculo de forma adequada para quem quer entender detalhes de
implementacao. Para uma abordagem mais te
orica desta linguagem, deve-se consultar os livros
de Bruce MacLennan [24] e de Antony Davie [7].

42

Captulo 3

Programa
c
ao funcional em Haskell
There are many distint pleasures
associated with Computer programming.
Craftsmanship has its quiet rewards, the satisfaction that
comes from building a useful object and making it work.
(Steven S. Skiena et Miguel A. Revilla in [33])

3.1

Introdu
c
ao

Os Captulos anteriores foram feitos com o objetivo de servirem como preparacao e fundamentacao para o estudo das linguagens funcionais, em particular, de Haskell. Este Captulo e os
seguintes sao todos dedicados `a codicacao de programas funcionais utilizando esta linguagem.
A comunidade das linguagens funcionais tem dado a Haskell uma atencao especial e, por este
motivo, muita pesquisa tem sido feita tentando dot
a-la de caractersticas que a torne uma linguagem de uso popular. Estas caractersticas foram citadas por Philip Wadler [37] e analisadas
na Introducao desta Apostila.
Haskell e uma linguagem funcional pura, n
ao estrita, fortemente tipada, cujo nome e uma
homenagem a Haskell Brooks Curry, um estudioso da logica combinatorial e um dos mais pro uma linguagem baseada em scripts, que
eminentes pesquisadores sobre -calculo [4, 35]. E
consistem em um conjunto de denicoes associadas a nomes, em um arquivo.
Em 1998, a comunidade de Haskell padronizou Haskell98 como a versao a ser utilizada ate
a denicao de Standard Haskell. No entanto, a linguagem continua sendo pesquisada buscando
a criacao de novas Bibliotecas a serem incorporadas ao sistema. Tambem muitas extensoes
estao sendo incorporadas, como Haskell paralelo, IDLs (Interface Description Language), por
exemplo HaskellDirect e interfaces para C e C++, permitindo integracao com estas e outras
linguagens. Em particular, tem sido desenvolvida AspectH, uma extensao de Haskell para
suportar orientacao a aspectos [2], alem de uma extensao para OpenGL.
O site ocial na WEB sobre Haskell e: http://www.haskell.org, onde todas as informacoes
sobre ela podem ser encontradas, alem de varios links para compiladores e interpretadores para
a linguagem.
O interpretador mais popular de Haskell e Hugs, desenvolvido por Mark Jones na Universidade de Nottingham e da Universidade de Yale. Esta implementacao, codicada em C,
e pequena, f
acil de ser usada e disponvel para v
arias plataformas, incluindo UNIX, Linux,
Windows 3.x, Win32, DOS e ambiente Macintosh.
Para produzir c
odigo executavel de maquina, foram desenvolvidos v
arios compiladores. Na
Universidade de Glasgow, foi construdo GHC (Glasgow Haskell Compiler) disponvel para
43

considerado
ambientes UNIX (Linux, Solaris, *BSD e MacOS-X) e tambem para Windows. E
um pouco lento e necessita de muita memoria. Est
a disponvel de forma livre em
http://www.dcs.gla.ac.uk/fp/software/ghc/
Na Universidade de Chalmers, foram desenvolvidos o interpretador HBI (Haskell-B Interpreter) e o compilador HBC (Haskell-B Compiler) disponveis em
http://www.cs.chalmers.se/ augustss/hbc.html.
Tambem esta disponvel o compilador nhc98, considerado f
acil de ser instalado, com heap
profiles, muito menor que os outros compiladores, disponvel para todos os padr
oes UNIX e
escrito em Haskell 98.

Existe ainda uma linguagem e um compilador, Helium, dedicados ao ensino de Haskell. E


um 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 primeira
delas e considerar todo o texto como um programa, exceto o que e comentado, que pode ser
de duas maneiras: com - -, que representa um comentario ate o nal da linha corrente, ou
envolvendo o coment
ario com os smbolos {- e -}, podendo englobar v
arias linhas. Os
arquivos em Haskell com este tipo de programa devem ter a extensao .hs. Esta e a maneira mais
utilizada pelos programadores de Haskell.
A outra forma e considerar todo o texto como comentario e sinalizar o que deve ser realmente
um programa iniciando a linha com o sinal > identado. Neste caso, o arquivo deve ter extensao
.lhs. Vamos mostrar um exemplo de cada situacao.
{- ######################################################################
exemplo.hs
Este arquivo eh um exemplo de um arquivo .hs. Deve ser editado como arquivo
texto e salvo com a extensao .hs.
#########################################################################-}
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)
{-######################################################################-}
Agora vamos mostrar o mesmo exemplo usando a visao de literal, com a extensao .lhs. Deve
44

ser observado que os sinais de incio e m 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-denidas que comp


oe o arquivo
Prelude.hs, que podem ser utilizadas pelo usuario a partir do instante em que o interpretador
e carregado. Na chamada, e aberta uma secao, que permanece ativa enquanto o sistema estiver
em 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 ser
observados na Tabela 3.1.

Comando
:?
:e
:e exemplo.hs
:l exemplo.hs
:a exemplo.hs
:q

Tabela 3.1: Principais comandos de Hugs.


Acao realizada
Aciona o help
Chama o script atual
Edita o arquivo exemplo.hs
Carrega o script exemplo.hs e limpa outros arquivos carregados
Carrega o script exemplo.hs sem limpar os outros arquivos
Termina a secao

Para o usu
ario executar qualquer funcao do Prelude e necessario apenas chamar esta funcao
na linha de comandos (prompt), disponvel ap
os o interpretador ser carregado, seguida de seus
argumentos e apertar a tecla Enter. O resultado sera exibido imediatamente na linha seguinte
ap
os o prompt. O interpretador tambem pode ser usado como uma calculadora para avaliar
expressoes aritmeticas, booleanas, logartmicas, trigonometricas, etc. A forma de utilizacao e
45

apenas colocar a expressao no prompt e apertar Enter. Caso a funcao ou expressao a ser
executada esteja com seus parametros colocados corretamente, o resultado aparecera na linha
seguinte. Caso a chamada da funcao ou expressao n
ao esteja correta, sera exibida uma mensagem
de 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 direita
9
!, !!, //, > . >
>>=
.
8
**, ,
7
% , /, div, mod, rem, quot
6
+, :+
5
\\
:, ++, > + >
4
/=, <, <=, = =, >, >=,
elem, notElem
3
&&
2
||
1
:=
0
$

Os operadores podem ser inxos ou pre-xos. Normalmente os operadores aritmeticos sao


declarados como inxos, por exemplo, se usa a expressao 3 + 7, por ser esta a forma comumente
utilizada na Matematica. Ja as funcoes sao normalmente declaradas como pre-xas por ser a
forma mais utilizada nas demais linguagens de programacao. No entanto, os operadores inxos
podem ser utilizados como pre-xos, apenas colocando o operador entre parenteses. Por exemplo,
o operador + (inxo) pode ser aplicado como (+) 2 3 (pre-xo). Os operadores pre-xos
tambem podem ser aplicados como inxos, apenas colocando o operador entre aspas simples (o
acento agudo em ambos os lados do operador). Por exemplo, maxi 3 4 (pre-xo) pode ser
utilizado como 3 maxi 4 (inxo).
possvel tambem trocar a associatividade ou a prioridade de um operador. Para isso,
E
e necessario declarar explicitamente o tipo de associatividade e a prioridade da funcao. Por
exemplo, para trocar a associatividade e a prioridade da funcao toma, pode-se fazer a declaracao:
Infixl 7 toma
signicando que a funcao toma e inxa, associa-se pela esquerda e tem um nvel de prioridade
7. Se a prioridade for omitida, ser
a considerada igual a 9, por default. Seguem alguns exemplos
de chamadas `a calculadora de express
oes 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

3.2.2

Identificadores em Haskell

Os identicadores em Haskell sao sensveis a caracteres, ou seja, as letras mai


usculas sao distintas
das letras min
usculas. Os identicadores, devem ser iniciados, sempre, por uma letra mai
uscula,
se for um tipo, ou min
uscula, se for um outro identicador como uma vari
avel, uma constante
ou uma funcao. A esta primeira letra do identicador podem ser seguidos outros caracteres, que
podem ser letras mai
usculas ou min
usculas, dgitos, sublinhado ou acentos agudos. Por exemplo,
type Par = (Int, Int)
somaAmbos :: Par -> Int
somaAmbos (primeiro, segundo) = primeiro + segundo
Deve ser lembrado aqui que Haskell e uma linguagem funcional pura e, como tal, n
ao permite atribuicoes destrutivas, ou seja, nao e possvel fazer atualizacoes de vari
aveis em Haskell.
Isto signica que, nos exemlos mostrados anteriormente, a variavel resposta tera o valor 42
enquanto o script estiver ativo. Se for necessario atribuir outro valor para resposta, ter
a que
ser criada uma outra vari
avel para isto. Desta forma, em Haskell, as variaveis sao consideradas
sao consideradas constantes, uma vez que elas nao podem ser atualizadas.
As palavras reservadas da linguagem s
ao sempre escritas em letras min
usculas. Haskell
apresenta 22 palavras reservadas, mostradas a seguir:
case
class
data
default
deriving

3.3

else
hiding
if
import
in

infix
infixl
infixr
instance
let

module
of
renaming
then
to

type
where

Fun
c
oes em Haskell

As formas de denicao de funcoes em Haskell tem a ver com as formas de denicao de funcoes
utilizadas na Matematica, mostradas no Captulo 1. Em Haskell, elas podem ser pensadas e
representadas gracamente por uma caixa que recebe um ou mais parametros como entrada
(argumentos), processa-os e constroi um resultado u
nico que e exibido como sada, conforme
pode ser visto na Figura 3.1.
Int
Int

Quadro
Int

escala

Int

Quadro

Figura 3.1: Representacao gr


aca das funcoes + e escala.
Exemplos de fun
c
oes:
Uma funcao que calcula as razes de uma equacao bi-quadrada.
Uma funcao que emite o relatorio nal dos resultados com as notas parciais e nal dos
alunos da disciplina T
opicos em Linguagem de Programacao.
47

Uma funcao que controla a velocidade de um autom


ovel.
Mais exemplos de funcoes podem ser encontrados em qualquer atividade da vida. Na Figura
3.2 estao mostradas gracamente 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 e
o elemento de entrada da funcao que o processa e transforma em outro Quadro. Por exemplo a
funcao espelhaV toma como argumento um Quadro e faz o espelhamento deste Quadro em
relacao 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 apenas
mostrar exemplos de funcoes. A modelagem destes objetos serao feitas ao longo do texto.

espelhaV

espelhaH

invertecor

escala

sobrepoe

Figura 3.2: Resultados gracos de funcoes.


No entanto, e necessario salientar a import
ancia que os tipos dos argumentos e dos resultados
tem nas denicoes de funcoes. Eles permitem ao programador estabelecer uma correspondencia
bem denida entre eles e os objetos que modelam, proporcionando uma simulacao adequada.
Assim, as funcoes desta Figura tem os tipos:
espelhaV :: Quadro -> Quadro
espelhaH :: Quadro -> Quadro
invertecor :: Quadro -> Quadro
escala :: Quadro -> Quadro
sobrepoe :: Quadro -> Quadro
48

3.3.1

Construindo fun
c
oes

Um tipo de dado e uma colecao de valores onde todos eles tem as mesmas caractersticas.
Por exemplo, os n
umeros inteiros, os caracteres, os strings de caracteres, etc. As funcoes em
Haskell podem ser declaradas com o seu nome, seguido de ::, vindo em seguida os tipos de seus
argumentos, um a um, com uma echa ( >) entre eles e, nalmente mais uma echa seguida
do tipo do resultado que a aplicacao da funcao produz. Por exemplo, as funcoes + e escala,
mostradas gracamente na Figura 3.1, tem seus tipos denidos da forma a seguir:
+ :: Int -> Int -> Int e
escala :: Quadro -> Int -> Quadro
O tipo da funcao e daclarado em sua forma curricada, onde uma funcao de n argumentos
nico argumento, da mesma forma adotada pelo -calculo,
e considerada como n funcoes de um u
vista no Captulo 2.
Os tipos nos d
ao informacoes importantes sobre a aplicacao das funcoes. Por exemplo, a
declaracao da funcao escala, mostrada anteriormente, nos informa que:
a funcao escala tem dois argumentos de entrada, sendo o primeiro do tipo Quadro e o
segundo do tipo Int e
o resultado da aplicacao da funcao e um valor do tipo Quadro, facilitando o entendimento
do problema e a forma de solucao adotada para resolve-lo.
Alem dos tipos, a declaracao de uma funcao deve exibir explicitamente como o processamento
de seus argumentos deve ser feito ou pode declarar que este processamento seja feito atraves
de denicoes ja feitas para outras funcoes, sendo esta uma forma muito comum de declaracao.
Como exemplo do primeiro caso, podemos vericar a denicao da funcao somaAmbos. A
forma 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 funcao
rotaciona, que promove uma rotacao em torno da origem do sistema de coordenadas cartesianas
x0y, pode ser denida em funcao das funcoes espelhaV e espelhaH, denidas anteriormente.
rotaciona :: Quadro -> Quadro
rotaciona pic = espelhaH (espelhaV pic)
Algumas linguagens funcionais exigem que os tipos das funcoes sejam declarados explicitamente pelo programador. Em Haskell, seus projetistas optaram por deixar que os tipos das
funcoes sejam inferidos pelo sistema. Isto signica que e opcional a declaracao dos tipos das
funcoes pelo programador. Mesmo assim, encoraja-se que esta declaracao seja feita explicitamente, como forma de disciplina de programacao. Isto permite ao programador um completo
entendimento do problema e da solucao adotada.
Voltando ao exemplo da funcao rotaciona, ela tambem pode ser codicada de outra forma,
mais elegante, utilizando a composicao de funcoes, uma propriedade implementada apenas nas
linguagens funcionais. Esta forma de construcao de funcoes sera vista com detalhes ainda neste
Captulo, no entanto, ela pode ser feita em Haskell usando a notacao de ponto (.), a mesma
adotada para a composicao de funcoes na Matem
atica.
rotaciona

espelhaH . espelha
49

Modelagem. Suponhamos que um cavalo seja um objeto do tipo Quadro (j


a visto anteriormente). 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 cavalo
e uma lista de listas de caracteres1 , conforme pode ser visto gracamente na representacao a
seguir, onde os pontos estao colocados apenas para facilitar a contagem, ou seja, eles estao colocados apenas para representar os caracteres em branco. Os resultados das aplicacoes das funcoes
espelhaH 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 funcoes
j
a denidas para outras nalidades, o que proporciona ainda mais exibilidade ao programador.
Por exemplo,
espelhaH cav = reverse cav
espelhaV cav = map reverse cav

--inverte os elementos de uma lista

A funcao reverse, quando aplicada a uma lista de elementos de qualquer tipo d


a, como
resultado, uma outra lista com os mesmos elementos da primeira lista, na ordem inversa. Por
exemplo, reverse [1,2,3] = [3,2,1] e reverse [a, b, c] = [c, b, a]. Isto signica
que a funcao reverse aplicada a cada linha do cavalo, que e uma lista de caracteres, da, como
resultado, a mesma linha mas com a ordem de seus caracteres invertida. A funcao map toma
a funcao reverse, o primeiro de seus dois argumentos, e a aplica a cada uma das linhas de seu
segundo argumento (um cavalo). As funcoes map e reverse sao pre-denidas em Haskell e elas
serao objeto de estudo mais profundo nas pr
oximas 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 signica
que uma mesma funcao pode ser aplicada a mais de um tipo de dados. Isto signica
polimorsmo, ou seja, a utilizacao generica de uma funcao, aumentando a produtividade
de software.
1

O tipo lista e um tipo primitivo em Haskell (o tipo mais importante nas linguagens funcionais) e, dada a sua
import
ancia, ser
a dedicado, um Captulo completo (o Captulo 4) sobre a sua construca
o e utilizaca
o.

50

Um argumento da funcao map e reverse e reverse e tambem uma funcao. Isto signica
que uma funcao (reverse) pode ser passada como par
ametro para uma outra funcao.
Neste caso, diz-se que as func
oes s
ao cidad
aos de primeira categoria, permitindo a elas os
mesmos direitos que qualquer outro tipo de dado.
O resultado da aplicacao da funcao map `a funcao reverse e uma outra funcao que e
aplicada a um outro argumento que, neste caso, e uma lista. Neste caso, diz-se que, nas
linguagens funcionais, as func
oes s
ao de alta ordem, ou seja, podem ser passadas como
argumentos de uma funcao e tambem podem retornar como resultados da aplicacao de
uma funcao.

3.3.2

Avalia
c
ao de func
oes em Haskell

A forma de avaliacao de funcoes utilizada em programas codicados em Haskell e a ordem normal


de avaliacao, preconizada pelo segundo teorema de Russell visto no Captulo 2. Isto signica
que 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 m
aximo uma vez.
Isto signica um tipo de avaliacao semelhante `a avaliacao curto-circuito utilizada em algumas
linguagens convencionais. Vejamos um exemplo de avaliacao usando as funcoes denidas nos
scripts mostrados no incio deste Captulo.
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 padr
oes (patterns matching)

O casamento de padr
oes e outra forma de codicacao de funcoes em Haskell, baseada na denicao por enumeracao utilizada na Matematica, vista no Captulo 1. Neste tipo de denicao,
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 do
mapeamento (entrada, resultado). Por exemplo, vejamos as declaracoes das funcoes eZero e
fat, a seguir:
eZero :: Int -> Bool
eZero 0 = True
eZero _ = False

fat :: Int -> Int


fat 0 = 1
fat n = n * fat (n --1)

Na execucao de aplicacoes destas funcoes, os padroes sao testados sequencialmente, de cima


para baixo. O primeiro padr
ao que casa com o valor da entrada ter
a o valor correspondente
como resultado da aplicacao da funcao. Se n
ao acontecer quelquer casamento entre o valor de
entrada e um padr
ao de entrada, o resultado da aplicacao da funcao sera um erro. Ao se aplicar
a funcao eZero a um argumento n, primeiro e vericado se este n
umero n casa com 0. Se for
verdade, o resultado ser
a True. Se este padr
ao n
ao for vericado, ou seja, se n n
ao for igual
a 0, verica-se o casamento com o segundo padrao e assim por diante. Neste caso, o resultado
sera False. O mesmo reciocnio vale para a funcao fat.
Esta forma de an
alise seq
uencial deve sempre ser levada em consideracao para que erros
grosseiros sejam evitados. Como exemplo, se, na declaracao da funcao eZero, a denicao da
51

funcao para o caso de n ser 0 for trocada pela segunda denicao, o resultado da aplicacao da
funcao sera sempre igual a False, mesmo que o argumento seja 0, uma vez que o (undescore)
signica qualquer caso.
Exerccios:
1. De a denicao da funcao todosQuatroIguais do tipo Int >Int >Int >Int >Bool
que d
a o resultado True se seus quatro argumentos forem iguais.
2. De denicao da funcao todosQuatroIguais usando a denicao da funcao todosIguais,
dada anteriormente.
3. O que esta errado com a denicao 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 >Int
teste n m p = ((n+m+p) == 3*p).
Esta funcao se comporta da mesma forma que a funcao todosIguais para o seu teste de
dados? Que conclus
ao voce tira sobre os testes em geral?
5. De uma denicao para a funcao quantosIguais usando as funcoes todosIguais e todosDiferentes.
6. Escreva a sequencia de calculos para as seguintes express
oes:
maximo ((2 + 3) 7) (4 + (1 3))
todosIguais 4 quadrado 2 3
quantosIguais 3 4 3

3.4

Tipos de dados em Haskell

Haskell, a exemplo de qualquer linguagem de programacao, prove uma colecao de tipos primitivos
e tambem permite tipos estruturados, denidos pelo programador, provendo grande exibilidade
na 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), o
tipo caractere (Char), o tipo cadeia de caracteres (String) e o tipo ponto utuante (Float ou
Double) e o tipo lista. Nesta secao, vamos analisar cada um destes tipos primitivos, deixando
o tipo lista para ser tratado no Captulo 4, dada a sua import
ancia nas linguagens funcionais e,
em particular, em Haskell. Tambem serao estudados os tipos estruturados possveis em Haskell.
O tipo inteiro (Int ou Integer)
Como em muitas linguagens, o tipo inteiro e primitivo em Haskell. Seu domnio de valores
e o mesmo das outras linguagens. Os valores do tipo Integer sao representados com o dobro
da quantidade de bits necess
arios para representar os valores do tipo Int. Seus operadores
aritmeticos sao os mesmos admitidos na maioria das outras linguagens:
Operadores aritm
eticos para valores dos tipos Int ou Integer.
52

+, *

div
mod
abs
negate

adicao e multiplicacao
exponenciacao
subtracao (inxa) e inversor de sinal (prexa)
divis
ao inteira (prexa), ou div (inxa)
modulo (prexa), ou mod (inxa)
valor absoluto de um inteiro
troca o sinal de um inteiro

Os operadores relacionais tambem sao os mesmos encontrados na grande maioria das linguagens.
Operadores relacionais: Int >Int >Bool
>, >=, ==, / =, <=, <
Vejamos um exemplo simples de construcao de funcao usando inteiros.
mdc :: Int -> Int -> Int
mdc 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 envolve
operacoes com inteiros. Seja uma empresa que necessita de respostas para as questoes a seguir, para fundamentar suas tomadas de decis
oes:
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 n
ao houve vendas? (se houve alguma).
Vamos construir algumas funcoes em Haskell para responder a algumas destas questoes,
deixando algumas outras para exerccio do leitor. Para isto e necessario que recordemos as
denicoes matematicas de funcoes, vistas no Captulo 1.
Quest
ao 1:
Para solucionar a Quest
ao 1, devemos inicialmente construir uma funcao vendas i que
vai nos informar qual o valor das vendas na semana i. Esta funcao pode ser construda de
v
arias formas, no entanto ser
a feita aqui uma denicao por casamento de padr
oes, semelhante
`a denicao por enumeracao da Matem
atica.
vendas
vendas
vendas
vendas

:: Int -> Int


0 = 7
1 = 2
2 = 5

Veriquemos agora como as denicoes recursivas utilizadas na Matematica podem ser utilizadas em Haskell. Recordemos a denicao matematica de uma funcao usando recurs
ao. Por
exemplo, na denicao de uma funcao fun, devemos:
53

explicitar o valor de fun 0, (caso base)


explicitar o valor de fun n, usando o valor de fun (n - 1) (caso recursivo ou passo
indutivo).
Trazendo esta forma de denicao para o nosso caso, vamos construir a funcao totaldeVendas
i a partir de vendas i, que vai mostrar o total de vendas realizadas ate a semana i, inclusive.
totaldeVendas :: Int->Int
totaldeVendas 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 s
ao
avaliadas tambem seq
uencialmente, da mesma forma feita com o casamento de padroes. A
a
palavra reservada otherwise exerce um papel parecido, mas diferente, do (sublinhado), j
descrito anteriormente. A cl
ausula otherwise deve ser utilizada apenas quando houver guardas
anteriores, cujos resultados sejam todos False. Isto signica que se a clausula otherwise for
colocada na primeira denicao de uma funcao, ocorrer
a um erro, uma vez que n
ao existem
ao ocasionara erro.
guardas denidas anteriormente. No caso do (sublinhado), esta situacao n
Usando a denicao de vendas i e de totaldeVendas i, dadas anteriormente, podemos
calcular 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

Quest
ao 2:
Vamos agora denir 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 funcao
maxVendas 0 sera vendas 0. Se a maior venda ocorreu ate a semana n, pode estar ate a
semana (n-1) ou sera vendas n.
maxVendas :: Int -> Int
maxVendas n
|n == 0
|maxVendas (n-1) >= vendas n
|otherwise

= vendas 0
= maxVendas (n - 1)
= vendas n

Esta mesma funcao pode ser construda de outra forma, usando uma funcao auxiliar, maximo, que aplicada a dois inteiros retorna o maior entre eles.
maximo :: Int -> Int -> Int
maximo x y
54

|x >= y
= x
|otherwise = y
maxVendas n
|n == 0
|otherwise

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

Esta forma de denicao de funcoes e chamada recurs


ao primitiva.
E se chamarmos totaldeVendas (-2)? O resultado ser
a = ERROR: Control stack
overflow, uma vez que a pilha do sistema vai estourar. Esta entrada caracteriza uma exce
c
ao
e, como tal, deve ser tratada. Em Haskell, existem varias formas de tratar excecoes, mas elas
serao vistas no Captulo 6. Apenas para exemplicar, uma forma de fazer isto e denir um valor
ctcio para o caso de n ser negativo, transformando a denicao 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 denir funcoes:
1. quadrado x = x * x
2. maximo n m
| n >= m
| otherwise

= n (equacao condicional)
=m

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


Exerccios
1. Dena uma funcao para encontrar a semana em que ocorreu a venda m
axima entre a
semana 0 e a semana n. O que sua funcao faz se houver mais de uma semana com vendas
maximas?
2. Dena uma funcao para encontrar uma semana sem vendas entre as semanas 0 e n. Se
n
ao existir tal semana, o resultado deve ser n + 1.
3. Dena uma funcao que retorne o n
umero de semanas sem vendas (se houver alguma).
4. Dena uma funcao que retorna o n
umero 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 denicao vendas n = n mod 2 + (n + 1) mod
3
6. De uma denicao da funcao fat que calcula o fatorial de n, onde n e um inteiro positivo.
7. De uma denicao de uma funcao de m e n que retorna o produto
m * (m + 1) * ... *(n 1) * n.
8. De uma denicao de uma funcao que retorne i-esimo n
umero da sequencia de Fibonacci
(0, 1, 1, 2...).
55

O tipo booleano (Bool)


Os u
nicos valores booleanos sao True e False e sobre eles podem ser utilizadas funcoes
pre-denidas ou funcoes construdas pelo usu
ario. As funcoes pre-denidas s
ao:
Funcao
&&
||
not

Nome
and
or
inversor

Tipo
&& :: Bool > Bool > Bool
|| :: Bool > Bool > Bool
not :: Bool > Bool

A funcao OU exclusivo pode ser denida pelo usu


ario da seguinte forma:
exOr :: Bool -> Bool -> Bool
exOr True x = not x
exOr False x = x
Exerccios
1. De a denicao de uma funcao nAnd :: Bool > Bool > Bool que d
a o resultado
True, exceto quando seus dois argumentos sao ambos True.
2. Dena uma funcao numEquallMax :: Int > Int > Int > Int onde numEquallMax n m p retorna a quantidade de n
umeros iguais ao maximo entre n, m e
p.
3. Como voce simplicaria 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 caracteres
que sao especiais por terem utilizacoes especcas. Entre eles se encontram:
\t - tabulacao
\n - nova linha
\\ - uma barra invertida

\ - aspas simples
\ - aspas duplas
\34 - ?

Existem algumas funcoes pre-denidas em Haskell feitas para converter caracteres em n


umeros
e vice-versa.
toEnum
:: Int -> Char
fromEnum :: Char -> Int
O tipo cadeia de caracteres (String)
O tipo cadeia de caracteres, normalmente chamado de String, tem uma caracterstica peculiar em Haskell. Apesar deste caso ser considerado um caso patologico por alguns pesquisadores
de linguagens de programacao, os criadores de Haskell admitiram duas formas para este tipo.
Ele e um tipo pre-denido como String, mas tambem pode ser considerado como uma lista de
caracteres. Para satisfazer as duas formas, as strings podem ser escritas entre aspas duplas ou
usando a notacao de lista de caracteres (entre aspas simples). Por exemplo, Constantinotem
o mesmo signicado que [C, o, n, s, t, a, n, t, i, n, o], em Haskell.
56

Todas as funcoes polim


orcas do Prelude.hs podem ser vericadas no pr
oximo Captulo.
Estas funcoes podem ser usadas sobre strings, uma vez que elas sao denidas sobre listas de
algum tipo e uma string e tambem uma lista.
No entanto, deve ser feita uma observacao sobre a forma como uma string e exibida no monitor ou em outro dispositivo de sada. As formas como Haskell trata as entradas e sadas de seus
dados s
ao descritas no Captulo 6, uma vez que seu entendimento requer um amadurecimento
tecnico do leitor em relacao `
as linguagens funcionais, presumindo-se que ele ainda n
ao o tenha
neste ponto.
Toda aplicacao de funcao, em Haskell, produz um resultado. Podemos vericar isto atraves
da tipicacao das funcoes. Ocorre, no entanto, que para mostrar um resultado, no monitor
ou em outro dispositivo de sada, e necessario denir uma funcao para esta tarefa. E qual
deve ser o resultado desta operacao? Mostrar um valor no monitor n
ao implica em retorno
de 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 este
tipo de comunicacao, adota uma semantica de acoes baseada em M
onadas, uma teoria bastante
complexa. 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 na
comunicacao com o mundo exterior e formas de aplicacao para facilitar o entendimento pelo
leitor. Caso contrario, seria bastante tedioso usar funcoes sem poder vericar os resultados de
suas aplicacoes.
A primeira destas funcoes pre-denidas em Haskell e putStr :: String > IO () que e
utilizada para mostrar strings no monitor. Assim,
putStr \99a\116 = cat
putStr Dunga\teh o bicho = Dunga
eh o bicho
putStr jeri"++ qua" ++ quara" = jeriquaquara
Neste caso, as strings podem ser mostradas na tela do monitor. E se um valor nao for um
string? Neste caso, uma solucao e transformar o valor em uma string e agora pode-se usar a
funcao putStr. Para esta miss
ao, foi denida a funcao show :: t > String que transforma
um valor de qualquer tipo em uma string. Alem destas, a funcao read :: String > t toma
uma string como argumento de entrada e a transforma em um valor. Por exemplo,
show (5+7) = 12
show (True && False) = False
read True = True
read 14 = 14
Exerccios:
1. Dena uma funcao para converter letras min
usculas em mai
usculas e que retorne o pr
oprio
caractere se a entrada nao for um caractere min
usculo.
2. Dena uma funcao charParaInt :: Char > Int que converte um dgito em seu valor
(por exemplo, 8 em 8). O valor de um caractere nao dgito deve ser 0 (zero).
3. Dena uma funcao imprimeDigito :: Char > String que converte um dgito em sua
representacao em portugues. Por exemplo, 5 deve retornar cinco.
57

4. Dena uma funcao romanoParaString :: Char > String que converte um algarismo
romano em sua representacao em Portugues. Por exemplo, romanoParaString V =
cinco.
5. Dena uma funcao emTresLinhas :: String > String > String > String que
toma tres strings e retorna um u
nico string mostrando os tres strings em linhas separadas.
6. Dena uma funcao replica :: String > Int > String que toma um String e um
n
umero natural n e retorna n copias da String, todas juntas. Se n for 0, o resultado deve
ser a String vazia (), se n for 1, retorna a pr
opria String.
O tipo ponto flutuante (Float ou Double)
Os valores do tipo ponto utuante (n
umeros reais) pertencem aos tipos Float ou Double,
da mesma forma que um n
umero inteiro pertence aos tipos Int ou Integer. Isto signica que
as u
nicas diferencas entre valores destes tipos se vericam na quantidade de bits usados para
represent
a-los. A Tabela 3.3 mostra as principais funcoes aritmeticas pre-denidas na linguagem.
As funcoes aritmeticas recebem a denominacao especial de operadores.
Tabela 3.3: Operadores aritmeticos de ponto utuante em Haskell.
+, - *
Float > Float > Float
/
Float > Float > Float

Float > Int > Float


**
Float > Float > Float
==, /= <, >,<=, >= Float > Float > Bool
abs
Float > Float
acos, asin, atan
Float > Float
ceiling, oor, round
Float > Float
cos, sin, tan
Float > Float
exp
Float > Float
fromInt
Int > Float
log
Float > Float
logBase
Float > Float > Float
negate
Float > Float
read
String > Float
pi
Float
show
* > String
signum
Float > Int
sqrt
Float > Float

Apesar de alguns pesquisadores, mais puristas, condenarem a sobrecarga de operadores, alguns outros defendem que alguma forma de sobrecarga deve existir, para facilitar a codicacao
de programas. Os projetistas de Haskell adimitiram a sobrecarga de operadores para ser utilizada na implementacao de uma de suas caractersticas importantes e que permite um grau de
abstracao bem maior que normalmente se encontra em outras linguagens. Esta caracterstica se
refere `as classes de tipos (type class), um tema a ser analisado no Captulo 5.
Exerccio
Dena uma funcao mediadasVendas :: Int > Float onde mediadasVendas n e a
media aritmetica entre os valores de vendas 0 ate vendas n.
58

Tabela 3.4: Quantidade


Semana
0
1
2
Total
Media

3.4.2

de vendas por semana.


Vendas
12
14
15
41
13.6667

Programando com n
umeros e strings

Duas metodologias de construcao de programas, bastante difundidas, s


ao a programacao topdown e a programacao bottom-up, de plena aceitacao pelos Engenheiros de Software. Haskell
permite que estas duas tecnicas sejam utilizadas em programas nela codicados. Elas serao
mostradas a seguir.
Programa
c
ao top-down
Vamos nos referir novamente ao problema das vendas, descrito anteriormente. Suponhamos que
as quantidades de vendas sejam as mostradas na Tabela 3.4.
Podemos construir uma funcao, imprimeTab, a partir da concatenacao de outras funcoes
que constroem strings e ser
ao desenvolvidas a seguir.
imprimeTab :: Int -> String
imprimeTab n = cabecalho ++ imprimeSemanas n ++ imprimeTotal
++ imprimeMedia n
Agora e necessario que estas funcoes componentes sejam denidas. Por exemplo, a funcao
cabecalho e formada apenas por um string para representar os ttulos dos tens da Tabela 3.4.
cabecalho :: String
cabecalho = " Semana Vendas\n"
A funcao imprimeSemanas deve mostrar no monitor o n
umero de cada semana e a quantidade de vendas correspondente. Ela ser
a denida em funcao de uma outra funcao, imprimeSemana, que tambem tera que ser denida.
imprimeSemanas :: Int -> String
imprimeSemanas 0 = imprimeSemana 0
imprimeSemanas n = imprimeSemanas (n-1) ++ imprimeSemana n
Faltam ser denidas as funcoes imprimeSemana, imprimeTotal e imprimeMedia que
sao deixadas como exerccio para o leitor.
Programa
c
ao bottom-up
A programacao top-down parte de um caso mais geral para casos mais especcos, enquanto
a modelagem bottom-up tem o sentido inverso desta orientacao. Ela inicia com as denicoes
mais particulares, para depois compo-las em uma forma mais geral. Para exemplicarmos esta
situacao, vamos utilizar a funcao rJustify :: Int > String > String, onde rJustify 10
Maria=
Maria, a ser denida.
59

imprimeSemana :: Int -> String


imprimeSemana n = rJustify offset (show n) ++
rJustify offset (show (vendas n)) ++ "\n"
where offset :: Int
offset = 10
imprimeMedia :: Int -> String
imprimeMedia n = "\nMedia " ++ rJustify offset (show mediadeVendas n)
Exerccios:
1. Dena uma funcao espacos :: Int > String onde espacos n retorna um string de n
espacos em branco.
2. Use a funcao espacos para denir uma funcao rJustify mencionada anteriormente.
3. De uma denicao de uma funcao tabeladeFatoriais :: Int > Int > String que
mostre 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 o
segundo 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 pr


oprios tipos de dados, de acordo com as necessidades que ele tenha de simular problemas do mundo real. Os
tipos estruturados s
ao construdos a partir de outros tipos, primitivos ou estruturados. Esta e
uma caracterstica 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,
qu
adruplas, etc. Na maioria das linguagens de programacao imperativas, este tipo de dados e
implementado atraves de registros ou estruturas. Em Haskell, o tipo (t1 , t2 , ..., tn ) consiste de
n-uplas de valores (v1 , v2 , ..., vn ) onde v1 ::t1 , v2 ::t2 , ..., vn ::tn .
Por exemplo,
type Pessoa = (String, String, Int)
maria :: Pessoa
maria = ("Maria das Dores", "225-0000", 22)
intP :: (Int, Int)
intP = (35, 45)
As funcoes sobre tuplas, apesar de poderem ser denidas de varias formas, sao comumente
denidas por pattern matching.
somaPar :: (Int, Int) -> Int
somaPar (x, y) = x + y
Os padr
oes podem ter constantes e/ou padroes aninhados.
60

shift :: ((Int, Int), Int) -> (Int, (Int, Int))


shift ((a,b),c) = (a, (b,c))
Podem ser denidas funcoes para mostrar casos particulares de uma tupla:
nome :: Pessoa -> String
fone :: Pessoa -> String
idade :: Pessoa -> Int
nome (n, p, a) = n
fone (n, p, a) = p
idade (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 conduzir
a erros. Por exemplo, sao diferentes:
somaPar :: (Int, Int) -> Int
somaPar (a, b) = a + b

somaDois :: Int -> Int -> Int


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 denicao e a parte de um programa na qual ela e visvel e portanto pode ser
usada. Em Haskell, o escopo das denicoes e todo o script, ou seja, todo o arquivo no qual a
denicao foi feita. Por exemplo, vejamos a denicao de ehImpar n, a seguir, que menciona a
funcao ehPar, apesar desta ser denida depois. Isto so e possvel porque elas compartilham o
mesmo escopo.
ehImpar, ehPar :: Int -> Bool
ehImpar 0 = False
ehImpar n = ehPar (n-1)
ehPar 0 = True
ehPar n = ehImpar (n-1)
Defini
c
oes locais
Haskell permite denicoes locais atraves da palavra reservada where. Por exemplo,
somaQuadrados :: Int -> Int -> Int
somaQuadrados n m = quadN + quadM
where
quadN = n * n
quadM = m * m
As denicoes locais podem incluir outras denicoes de funcoes, alem de poder usar denicoes
locais a uma express
ao, usando a palavra reservada let.
61

let x = 3 + 2; y = 5 - 1 in x2 + 2*x*y - y
As denicoes locais sao visveis apenas na equacao onde elas foram declaradas. As vari
aveis
que aparecem do lado esquerdo da igualdade tambem podem ser usadas em denicoes locais, do
lado esquerdo. Por exemplo,
maximoQuadrado x y
|quadx > quady = quadx
|otherwise = quady
where
quadx
quady
quad
quad z

= quad x
= quad y
:: Int -> Int
= z * z

As denicoes locais podem ser usadas antes delas serem denidas e tambem podem ser usadas
em resultados, em guardas ou em outras denicoes locais. Como exemplo,
maximasOcorrencias :: Int -> Int -> Int -> (Int, Int)
maximasOcorrencias n m p = (max, quantosIguais)
where
max
= maximoDeTres n m p
quantosIguais
= quantosIguaisValor
maximoDeTres
:: Int -> Int -> Int
maximoDeTres a b c = maximo (maximo (a,
quantosIguaisValor :: Int -> Int -> Int

max n m p
-> Int
b), c)
-> Int -> Int

onde a funcao quantosIguaisValor pode ser denida de uma das formas mostradas na Tabela
a seguir.

quantosIguaisValor valor n m p
= ehN + ehM + ehP
where
ehN = if n == valor then 1 else 0
ehM = if m == valor then 1 else 0
ehP = if p == valor then 1 else 0

quantosIguaisValor valor n m p
= ehvalor n + ehvalor m + ehvalor p
where
ehvalor :: Int > Int
ehvalor x = if x == valor then 1 else 0

A construcao if c then e1 else e2 e valida em Haskell e avalia a express


ao booleana
c, tendo como resultado e1 (se c for True) ou e2 (se c for False). Na denicao da funcao
quantosIguaisValor o construtor if foi utilizado v
arias vezes.

3.4.5

C
alculos:

A avaliacao usada por Haskell e chamada de avaliac


ao preguicosa, onde cada express
ao e avaliada
apenas uma vez e se necessario. Um calculo s
o e realizado se for realmente necessario e seu valor
e colocado em uma celula da heap. Se ele for novamente solicitado, ja esta calculado e pronto
para ser utilizado. Por este motivo, Haskell, a exemplo de qualquer linguagem funcional e todas
as modernas linguagens de programacao, faz o gerenciamento din
amico de memoria de forma
automatica, ou seja, pelo sistema. Este processo de gerenciamento da memoria din
amica de
forma autom
atica e chamado de Garbage Collection ou Coleta de Lixo. Para mais detalhes
sobre Coleta de Lixo, o leitor deve consultar as referencias [8, 9, 10, 11, 12].
62

Vamos mostrar, detalhadamente, a seq


uencia de avaliacao da aplicacao de duas funcoes ja
denidas anteriormente, somaquadrados e maximasOcorrencias.
somaQuadrados 4 3 = quadN + quadM
where
quadN = 4 * 4 = 16
quadM = 3 * 3 = 9
= 16 + 9
= 25
maximasOcorrencias 2 1 2 = (max, quantosIguais)
where
max = maximoDeTres 2 1 2
= maximo (maximo 2 1) 2
?? 2>=1 = True
= maximo 2 2
?? 2>=2 = True
= 2
= (2, quantosIguais)
where
quantosIguais
= quantosIguaisValor 2 2 1 2
= ehValor 2 + ehvalor 1 + ehvalor 2
where
ehvalor 2 = if 2 == 2 then 1 else 0
= if True then 1 else 0
= 1
= 1 + ehvalor 1 + ehvalor 2
where
ehvalor 1 = if 1 == 2 then 1 else 0
= if False then 1 else 0
= 0
= 1 + 0 + ehvalor 2
where
ehvalor 2 = if 2 == 2 then 1 else 0
= if True then 1 else 0
= 1
= 1 + 0 + 1
= 2
= (2, 2)
Esta seq
uencia de calculos deve ser acompanhada pelo leitor para entender a forma como
o compilador (ou interpretador) Haskell executa seus calculos. Este entendimento tem import
ancia fundamental na construcao de funcoes, notadamente de funcoes que tratam com listas
potencialmente innitas, a serem vistas mais adiante.
Exerccios:
1. Calcule os valores das expressoes: maximasOcorrencias 1 2 1 e quantosIguaisValor
4 2 1 3.
2. Dena uma funcao cJustify :: Int > String > String onde cJustify n st retorna
uma string de tamanho n, adicionando espacos antes e depois de st para centraliz
a-la.
63

3. Dena uma funcao stars :: Int > String de forma que stars 3 retorna ***. Como
deve ser tratada uma entrada negativa?

Exemplo. Vejamos agora construir um exemplo bastante conhecido que e o de encontrar as


razes reais de uma equacao do segundo grau, baseado em [35]. Neste caso teremos como entrada
a equacao a x2 + b x + c = 0, sendo a = 1.0, b = 5.0 e c = 6.0.
Para esta solucao, a sada ser
a a string
A equa
c
ao 1.0 * x 2 + 5.0 * x + 6.0 = 0.0
tem duas razes reais e distintas: -2.0 e -3.0.
Para isto vamos construir duas funcoes: umaRaiz para o caso da funcao ter duas razes
reais e iguais e duasRaizes para o caso dela ter duas razes reais e distintas.
umaRaiz :: Float -> Float -> Float -> Float
umaRaiz a b c = -b / (2.0 * a)
duasRaizes :: Float -> Float -> Float -> (Float, Float)
duasRaizes a b c = (d + e, d - e)
where
d = -b/(2.0*a)
e = sqrt (b^2 - 4.0*a*c)/(2.0*a)
saida :: Float -> Float -> Float -> String
saida a b c = cabecalho a b c ++ raizes a b c
cabecalho :: Float -> Float -> Float -> String
cabecalho 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 -> String
raizes 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 coeciente a for zero, n
ao sera possvel
a divis
ao de qualquer n
umero por ele. Neste caso, o programa deve abortar a execucao e uma
excecao deve ser feita para descrever o motivo. A funcao umaRaiz deve ser re-denida da
seguinte forma:
umaRaiz a b c
|(a /= 0.0) = -b/ (2.0 * a)
|otherwise = error "umaRaiz chamada com a == 0"
A redenicao da funcao duasRaizes e deixada para o leitor, como exerccio.
64

3.4.6

Projeto de programas

A Engenharia de Software admite algumas metodologias para a construcao de programas, de


forma a obter melhores resultados, tanto em relacao a`s solucoes quanto em relacao ao tempo de
desenvolvimento. A seq
uencia de passos mostrada a seguir, devida a Simon Thompson [35], e
considerada um bom roteiro na codicacao de programas para a solucao de problemas usando
Haskell:
Vericar 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 cl
ausulas where.
Se uma expressao aparecer mais de uma vez, e forte candidata a ser declarada como uma
funcao.
Usar uma abordagem bottom-up.
O layout do script e importante.
Exerccios
Para os exerccios a seguir, considere os pontos do plano como sendo do tipo Ponto =
(Float, Float). As linhas do plano s
ao denidas por seus pontos inicial e nal e tem o tipo
Linha = (Ponto, Ponto).
1. Dena funcoes que retornem a ordenada e a abcissa de um ponto.
2. Dena uma funcao que verica se uma linha e vertical ou n
ao.
3. Se uma linha e determinada pelos pontos (x1, y1) e (x2, y2), sua equacao e denida por
(y - y1)/(x - x1) = (y2 - y1)/(x2 - x1). Dena 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 l


ogica ou matematica para vericar se alguma premissa e ou
n
ao v
alida, em quaisquer circunst
ancias.
Este tema tem importancia fundamental na construcao de programas, uma vez que deve-se
ter a garantia de que o programa esteja correto e que ele realiza apenas a acao para a qual foi
criado. A prova de programas aumenta sua import
ancia a cada dia, uma vez que os problemas
estao se tornando cada vez mais complexos e deve-se ter a certeza de que o programa para resolvelo esteja correto. Os problemas se tornam cada vez mais desaadores, por exemplo, problemas
nucleares 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 usu
arios que, `as vezes, tem diculdades de realizar
provas de programas, achando ser uma tecnica de fundamentacao matematica e trabalhosa. A
prova de programas em uma linguagem imperativa e realmente tediosa, no entanto, como ser
a
visto, ela e bem menos difcil em uma linguagem funcional como Haskell.
65

Existem em Haskell tres maneiras de realizar provas: a prova direta, a prova por casos e a
prova por inducao matematica. Vamos analisa-las atraves de exemplos, lembrando ao leitor que
o domnio dessas tecnicas so e conseguido atraves da pr
atica.
Provas diretas
A prova direta e feita aplicando as denicoes das funcoes. Por exemplo, sejam as funcoes troca,
cicla e recicla, denidas 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 2
recicla (a, b, c) = (c, a, b)
--def 3
A partir destas denicoes, podemos provar assertivas nas quais estejam envolvidas. Por
exemplo, 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)
= (a, b)

--por def 1
--por def 1

cicla (recicla (a, b, c)) = cicla (c, a, b) = (a, b, c)


--por def 3 e 2
recicla(cicla (a, b, c)) = recicla (b, c, a) = (a, b, c) --por def 2 e 3
Portanto, s
ao iguais os resultados e a prova esta completa. Facil, n
ao?
Provas por casos
Seja a denicao da funcao maximo, j
a feita anteriormente no incio do Captulo:
maximo :: Int -> Int -> Int
maximo n m
|n >= m = n
--def 1
|otherwise = m
--def 2
Seja a assertiva: Para quaisquer n
umeros inteiros n e m, maximo n m n.
Para quaisquer n
umeros m e n denidos, tem-se: m > n ou n m. Ent
ao,
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 denir a funcao maxVendas da seguinte maneira:
maxVendas 0 = vendas 0
maxVendas r = maximo (maxVendas (r-1)) (vendas r)
66

Agora podemos provar que maxVendas r maxVendas (r - 1) usando a propriedade


mostrada para a funcao maximo.
Exerccios
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 que
somaTres (cicla (a, b, c)) = somaTres (a, b, c).
3. Dada a denicao
trocaSe :: (Int, Int) > (Int, Int)
trocaSe (a, b)
| a <= b
= (a, b)
| otherwise = (b, a)
Prove que para todo a e b denidos, trocaSe(trocaSe(a, b)) = trocaSe(a, b).
Indu
c
ao matem
atica
Da Matematica, sabemos que o esquema de prova por inducao dentro do conjunto dos n
umeros
naturais e uma forma muito comum. Um sistema similar pode ser utilizado para provar programas codicados 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
fatorial n = n * fatorial (n - 1)

-- (fat 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, signicando que
a 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 hip
otese de inducao informa que fatorial (n-1) > 0, ou seja, a propriedade P e valida
para 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 n
umeros positivos e tambem positivo. Logo,
maior que 0.
Conclus
ao: como a propriedade P e valida para o caso base e para o passo indutivo, ent
ao
ela e valida para todo n natural.
comum
Esta u
ltima parte, a conclus
ao da prova, e um componente importante da prova. E
ver um esquema de prova, normalmente feito por iniciantes, onde os dois passos s
ao vericados,
mas nao existe a conclusao. Neste caso, a prova esta incompleta.
67

Provas por Indu


c
ao
Enquanto a inducao formula provas para P(0), P(1), ..., a denicao recursiva de fatorial, vista
anteriormente, constr
oi resultados para fatorial 0, fatorial 1, .... A forma como P(n-1) e
assumida para se provar P(n) e semelhante `a forma usada por fatorial (n-1) para encontrar o
valor de fatorial (n).
Este esqema de prova normalmente e aplicado a funcoes denidas por recurs
ao primitiva
representando t
ao somente um processo de traducao semelhante ao esquema de prova por inducao
matematica.
Simon Thompson escreveu um guia de passos a serem seguidos nos esquemas de provas por
inducao em Haskell [35]. A seq
uencia de passos de provas proposta por ele e instrutiva e serve
de roteiro, principalmente para quem est
a dando os primeiros passos em direcao a este estudo.
Com o decorrer do tempo, este esquema passa a ser um processo automatico.
Est
agio 0:
Est
agio 1:
Est
agio 2:

Est
agio 3:
Est
agio 4:

escrever o objeto da prova em portugues,


escrever o objeto da prova em linguagem formal,
escrever os sub-objetos da prova por inducao:
P(0):
P(n), para todo n>0, assumindo P(n-1)
Provar P(0)
Provar P(n), para n>0, lembrando que deve e pode usar P(n-1)

Exemplo: Sejam as denicoes das funcoes a seguir:


power2 :: Int -> Int
power2 0 = 1
power2 r = 2 * power2 (r - 1)

(1)
(2)

sumPowers :: Int -> Int


sumPowers 0 = 1
sumPowers r = sumPowers (r-1) + power2 r

(3)
(4)

Prove que sumPowers n + 1 = power2 (n + 1).


Est
agio 0:
Est
agio 1:
Est
agio 2:

Est
agio 3:

provar que a soma das potencias de 2 de 0 a n, adicionada a 1


e igual a (n + 1)-esima potencia de 2.
provar P(n): sumPowers n + 1 = power2 (n+1)
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
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)

Exerccios
1. Prove que, para todo n
umero natural n, fatorial (n + 1) power2 n.
2. Prove que, para todo n
umero natural n, fib (n+1) power2 (n div 2).
68

3. De uma prova de que, para todo n


umero natural n, vendas n 0.

3.5

Resumo

Neste Captulo, foi dado incio ao estudo de programacao em Haskell. Foram vistos os tipos de
dados primitivos e as tuplas. Alguns exerccios foram resolvidos para dar uma nocao ao usu
ario
da 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 no
Captulo, por ser um livro que apresenta um conte
udo te
orico bem estruturado e fundamentado,
alem de muitos exerccios resolvidos e muitos problemas propostos.
O livro de Richard Bird [4] e outra fonte importante de exerccios resolvidos e propostos,
apesar de sua seq
uencia de abordagens seguir uma ordem distinta, exigindo um conhecimento
anterior sobre programacao funcional, o que o torna mais defcil de ser seguido por iniciantes
neste tema.
Outra referencia importante e o livro de Paul Hudak [14] que apresenta a programacao
funcional em Haskell com exemplos aplicados `a Multimdia, envolvendo a construcao de um
editor gr
aco e de um sistema utilizado em m
usica, entre outros. Para quem imagina que
Haskell s
o e aplicado a problemas da Matematica, esta referencia poe por terra este argumento.

69

70

Captulo 4

O tipo Lista
Commputers specially designed for applicative languages
implement recursive functions very eciently.
Also, architectural features can be included in conventional
computers that signicantly 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 linguagens
funcionais a implementam como um tipo primitivo, juntamente com uma gama imensa de funcoes
para a sua manipulacao. A notacao utilizada para as listas e colocar seus elementos entre
colchetes. Por exemplo, [1,2,3,4,1,3] e uma lista de inteiros, [True,False] e uma lista de
booleanos, [a, a, b] e uma lista de caracteres e [Marta, Marlene] e uma lista de
strings. H
a, no entanto, que se diferenciar as listas homogeneas, que s
ao as listas onde todos
os valores sao do mesmo tipo, das listas heterogeneas, onde os componentes podem ter mais de
um tipo. Haskell s
o admite listas homogeneas. Por exemplo, [False, 2,Maria] n
ao e uma
lista 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 pode
ser de inteiros, de booleanos, etc. Dito de outra forma, [ ] e do tipo [Int] ou [Bool] ou
[Int > Int], signicando que a lista vazia est
a na intersecao de todas as listas, sendo o
u
nico elemento deste conjunto.
a lista n
ao 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 de
a. O elemento a e chamado de cabe
ca e x e a cauda da lista.
Algumas caractersticas 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

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,
n
ao seria vazia.
A lista nao vazia tem cabeca e cauda, onde a cauda e tambem uma lista, que pode ser
vazia, ou n
ao.

4.1

Fun
c
oes sobre listas

As funcoes para a manipulacao de listas sao declaradas da mesma forma como sao declaradas
para processar outros tipos de dados, usando casamento de padr
oes. Neste caso, os padroes sao
apenas dois: a lista vazia e a lista nao vazia. Por exemplo,
somaLista :: [Int] -> Int
somaLista [ ] = 0
somaLista (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 ([ ]), a
soma sera 0. Se a lista nao for vazia (a : x), o resultado ser
a a soma de sua cabeca (a) com
o resultado da aplicacao da mesma funcao somaLista `a cauda da lista (x). Esta denicao
e recursiva, uma caracterstica muito utilizada, por ser a forma usada para fazer iteracao em
programas funcionais. Devemos observar que a ordem em que os padroes sao colocados tem
import
ancia fundamental. No caso em voga, primeiramente foi feita a denicao para a lista
vazia e depois para a lista n
ao vazia. Dependendo da necessidade do programador, esta ordem
pode ser invertida.
Vejamos a seq
uencia de calculos da aplicacao da funcao somaLista `a lista [2, 3, 5, 7].
somaLista [2,3,5,7] =2 +
=2 +
=2 +
=2 +
=2 +
=2 +
=2 +
=2 +
=17

4.1.1

somaLista [3,5,7]
(3 + somaLista [5,7])
(3 + (5 + somaLista [7]))
(3 + (5 + (7 + somaLista [])))
(3 + (5 + (7 + 0)))
(3 + (5 + 7))
(3 + 12)
15

O construtor de listas : (cons)

O construtor de listas, chamado de cons e sinalizado por : (dois pontos), tem import
ancia fundamental 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 da
nova lista. Por exemplo,
10 : [ ] = [10]
2 : 1 : 3 : [ ] = 2 : 1 : [3] = 2 : [1,3] = [2,1,3]
signicando que cons (:) associa seus componentes pela direita, ou seja:
72

a : b : c = a : (b : c) /= (a : b) : c
Mas qual o tipo de cons? Observando que 4 : [3] = [4, 3], ent
ao cons tem o tipo:
(:) :: Int -> [Int] -> [Int]
No entanto, vericamos tambem que True : [False] = [True, False]. Agora cons tem o
tipo:
(:) :: Bool -> [Bool] -> [Bool]
Isto mostra que o operador cons e polim
orco. 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 fun
c
oes sobre listas

Em Haskell, j
a existe um grande n
umero de funcoes pre-denidas para a manipulacao de listas.
Estas funcoes fazem parte do arquivo Prelude.hs, carregado no momento em que o sistema e
chamado e permanece ativo ate o nal da execucao. Um resumo destas funcoes, com seus tipos
e exemplos de utilizacao, pode ser visto na Tabela 4.1.
Tabela 4.1: Algumas funcoes polim
orcas 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=3
concat
[[a]] > [a]
[[2],[3,5]]=[2,3,5]
length
[a] > Int
length [3,2,1]=3
head
[a] > a
head [3,2,5]=3
last
[a] > a
last [3,2,1]=1
tail
[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]=False
or
[Bool] > Bool
or [True,False]=True
sum
[Int] > Int
sum [2,5,7]=14
[F loat] > F loat
sum [3.0,4.0,1.0]=8.0
product [Int] > Int
product [1,2,3]=6
[F loat] > F loat
product [1.0,2.0,3.0]=6.0

73

Alem das funcoes pre-denidas, o usu


ario tambem pode construir funcoes para manipular
listas. Aqui a criatividade e o limite. Vamos mostrar isto atraves de um exemplo simples e
depois atraves de um exemplo mais complexo.
Vamos construir uma funcao que verica se um determinado elemento pertence, ou n
ao, a
uma lista. Para isto vamos construir a funcao fazParte:
fazParte :: [Int] -> Int -> Bool
fazParte [ ] b = False
fazParte (a:x) b = (a == b) || fazParte x b
Esta mesma funcao tambem pode ser codicada de outra forma, usando guardas:
fazParte [ ] b = False
fazParte (a:x) b
|a == b = True
|otherwise = fazParte x b
Exerccios
1. Dada a denicao 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. Dena uma funcao produtoLista :: [Int] > Int que retorna o produto de uma lista
de inteiros.
5. Dena uma funcao and :: [Bool] > Bool que retorna a conjuncao da lista. Por
exemplo, and [e1 , e2 , . . . , en ] = e1 &&e2 && . . . &&en (a conjuncao da lista vazia e True).
6. Dena uma funcao concatena :: [[Int]] > [Int] que concatena uma lista de listas
de 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 de
inteiros.
Uma forma de ordenar uma lista n
ao vazia e inserir a cabeca da lista no local correto, que
pode ser na cabeca da lista ou pode ser na cauda j
a ordenada. Por exemplo, para ordenar a
lista de inteiros [3,4,1], devemos inserir 3 na cauda da lista, j
a ordenada, ou seja em [1,4]. Para
que esta cauda ja esteje ordenada e necessario apenas chamar a mesma funcao de ordenacao,
recursivamente, para ela. Vamos denir uma funcao de ordenacao, ordena, que utiliza uma
funcao 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

A lista vazia e considerada ordenada por vacuidade. Por isto a primeira denicao. Para o
caso da lista n
ao vazia, devemos inserir a cabeca (a) na cauda j
a ordenada (ordena x). Falta
apenas denir 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 inserc
ao direta e o leitor deve observar a
simplicidade como ele e implementado em Haskell. Sugerimos comparar esta implementacao
com outra, em qualquer linguagem convencional. Alem disso, tambem se deve considerar a
possibilidade de aplicacao desta mesma denicao a` listas de varios tipos de dados, signicando
que a denicao pode ser polim
orca. Isto signica que, na denicao da funcao insere, a u
nica
operacao exigida sobre os valores dos elementos da lista a ser ordenada e que eles possam ser
comparados atraves da operacao . Uma lista de valores, de qualquer tipo de dados, onde esta
operacao seja possvel entre estes valores, pode ser ordenada usando esta denicao.
Exerccios
1. Mostre todos os passos realizados na chamada ordena [2, 8, 1].
2. Dena uma funcao numOcorre :: [t] > t > Int, onde numOcorre l s retorna o
n
umero de vezes que o tem s aparece na lista l.
3. De uma denicao dia funcao fazParte usando a funcao numOcorre, do tem anterior.
4. Dena uma funcao unico :: [Int] > [Int] que retorna a lista de n
umeros que ocorrem
exatamente uma vez em uma lista. Por exemplo, unico [2,4,2,1,4] = [1].

4.2

Pattern matching revisado

O casamento de padr
oes ja foi analisado anteriormente, mas sem qualquer profundidade e formalismo. Agora ele sera visto com uma nova roupagem, apesar de usar conceitos j
a conhecidos.
Os padr
oes em Haskell sao dados por:
valores literais como -2, C e True.
vari
aveis como x, num e maria.
o caractere

(sublinhado) casa com qualquer argumento.

ao tem de ser
um padr
ao de tuplas (p1 , p2 , ..., pk ). Um argumento para casar com este padr
da forma (v1 , v2 , ..., vk ), onde cada vi deve ser do tipo pi .
um construtor aplicado a outros padr
oes. Por exemplo, o construtor de listas (p1 : p2 ),
oes
onde p1 e p2 sao padr
Nas linguagens funcionais, a forma de vericar se um padr
ao casa, ou nao, com um dado e
realizada da seguinte maneira:
primeiramente, analisa-se se o argumento esta na forma correta e
75

depois associam-se valores `as vari


aveis 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 questao
e respondida atraves da seguinte lista de clausulas:
se p for uma constante, a casa com p se a == p.
se p for uma vari
avel, x casa com p e x sera associado com p.
se p for uma tupla de padr
oes (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 padr
oes (p1 : p2 ), a casa com p se a for uma lista nao vazia. Neste
caso, 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 sublinhado
age como se fosse um teste.
Exemplo. Seja a funcao zip denida da seguinte forma:
zip (a:x) (b:y) = (a, b) : zip x y
zip _ _ = [ ]
Se os argumentos de zip forem duas listas, ambas nao vazias, forma-se a tupla com as cabecas
das duas listas, que sera incorporada a` lista de tuplas resultante e o processo continua com a
aplicacao recursiva de zip `
as caudas das listas argumentos. Este processo continua ate que o
padr
ao de duas listas n
ao vazias falhar. No momento em que uma das listas, ou ambas, for
vazia, o resultado ser
a a lista vazia e a execucao da funcao termina. Vamos vericar o resultado
de 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)]
Exerccios
1. Dena uma funcao somaTriplas que soma os elementos de uma lista de triplas de n
umeros,
(c, d, e).
2. Dena uma funcao somaPar para somar os elementos de uma lista de pares de n
umeros,
((c,d), (e,f)).
3. Calcule somaPar [(2,3), (96, -7)], passo a passo.
4. Dena uma funcao unzip :: [(Int, Int)] > ([Int], [Int]) que transforma uma lista de
pares em um par de listas. Sugestao: dena 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

Exemplo: Agora vamos analisar um exemplo pr


atico da aplicacao de listas em Haskell, baseado
em Simon Thompson [35]. Seja um banco de dados denido para contabilizar as retiradas de
livros de uma Biblioteca, por v
arias pessoas. Para simular esta situacao, vamos construir uma
lista de tuplas compostas pelo nome da pessoa que tomou emprestado um livro e do ttulo do
livro. Para isto, teremos:
type Pessoa = String
type Livro = String
type BancodeDados = [(Pessoa, Livro)]
Vamos construir uma lista ctcia para servir apenas de teste, ou seja, vamos supor que, em
um 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 denir funcoes para realizar as seguintes tarefas:
1. Operacc
oes 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 est
a ou n
ao emprestado.
Uma funcao que informa a quantidade de livros que uma determinada pessoa tomou
emprestado.
2. Opera
c
oes de atualiza
c
ao:
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 para
servir de roteiro para a denicao das outras funcoes de consulta, deixadas, como exerccio, para
o 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 denicoes das funcoes de atualizacao:
tomaEmprestado :: BancodeDados -> Pessoa -> Livro -> BancodeDados
tomaEmprestado dBase pessoa titulo = (pessoa, titulo) : dBase
devolveLivro :: BancodeDados -> Pessoa -> Livro -> BancodeDados
devolveLivro ((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

Que motivos o leitor imagina que o programador tenha levado em conta na denicao da
funcao devolveLivro, a exemplo da funcao zip, denida anteriormente, preferindo apresentar
a denicao para o padr
ao de lista vazia ap
os a denicao para o padr
ao de lista n
ao vazia, quando
o normal seria apresentar estes padroes na ordem inversa?
Exerccio:
Modique o banco de dados da Biblioteca anterior e as funcoes de acesso, de forma que:
exista um n
umero maximo de livros que uma pessoa possa tomar emprestado,
exista uma lista de palavras-chave associadas a cada livro, de forma que cada livro possa
ser encontrado atraves das palavras-chave a ele associadas, e
existam datas associadas aos emprestimos, para poder detectar os livros com datas de
emprestimos vencidas.

4.3

Compreens
oes e express
oes ZF (Zermelo-Fraenkel)

As compreens
oes, tambem conhecidas como expressoes ZF, sao devidas a Zermelo e Fraenkel e
representam uma forma muito rica de construcao de listas. O domnio desta tecnica permite
ao programador resolver muitos problemas de maneira simples e, em muitos casos, inusitada.
A sintaxe das expressoes ZF e muito pr
oxima da descricao matematica de conjuntos por intensionalidade, exprimindo determinadas propriedades. As diferencas se vericam apenas nos
sinais utilizados nas representacoes, mas a logica subjacente e a mesma. Vamos mostrar estas
semelhancas atraves de exemplos e depois vamos formalizar sua sintaxe.
Vamos supor que ex = [2,4,7]. Usando ex, podemos construir ex1, a lista cujos elementos
sejam 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 de
ex que sejam pares, podemos declarar
ex2 = [a | a<-ex, a mod 2 == 0]
Neste caso, ex2 = [2,4].
A partir destes exemplos, podemos vericar que a sintaxe das express
oes ZF e realmente
simples. Formalmente ela e dada da seguinte forma:
[ e | q1 , ..., qk ] onde cada qi e um qualicador, que pode ter umas das seguintes formas:
1. pode ser um gerador do tipo p< lExp, onde p e um padr
ao e lExp e uma expressao do
tipo lista, ou
2. pode ser um teste do tipo bExp, uma expressao booleana.
Propriedades de uma express
ao ZF
Os geradores podem ser combinados com nenhuma, uma ou mais express
oes booleanas.
Sendo ex a lista do Exemplo anterior, ent
ao
[2*a | a <- ex, a mod 2 == 0, a > 3] = [8]
78

Pode-se usar qualquer padr


ao `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]
possvel colocar m
E
ultiplos geradores e combinar geradores e testes.
aveis
Uma express
ao lExp ou bExp que aparece em um qualicador qi pode referenciar vari
usadas nos padr
oes dos qualicadores q1 ate qi1.
O algoritmo quicksort
Na secao 4.1.3, mostramos como o algoritmo de ordenacao por insercao direta pode ser implementado 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 algoritmo quicksort seja aplicado a uma lista de inteiros, ressaltando que ele tambem pode ser
aplicado a listas de qualquer tipo de dados, desde que estes dados possam ser comparados pelas
relacoes de ordem: maior, menor e igual.
O algoritmo quicksort utiliza o metodo de divis
ao e conquista em seu desenvolvimento. Em
sua implementacao, escolhe-se um elemento, o pivot, e a lista a ser ordenada e dividida em duas
sub-listas: uma contendo os elementos menores ou iguais ao pivot e a outra contendo os elementos
da lista que sejam maiores que o pivot. Neste ponto, o algoritmo e aplicado recursivamente `a
primeira e a` segunda sub-listas, concatenando seus resultados, com o pivot entre elas. A escolha
do pivot, normalmente, e feita pelo elemento do meio da lista, na expectativa de que ele esteja
pr
oximo da media da amostra. No entanto, esta escolha e apenas estatstica 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 seq
uencia 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 denir 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

Esta denicao pode tambem ser feita usando denicoes locais, tornando-a mais f
acil de ser
compreendida, 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]
N
ao e fant
astica esta denicao? Sugiro ao leitor vericar a implementacao deste algoritmo
utilizando alguma linguagem imperativa como C, C++ ou Java e observando as diferencas em
facilidade 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 tri
angulos 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)]
Coment
arios
No exemplo 1) deve ser observada a forma como as expressoes sao construdas. O primeiro
elemento escolhido, a, vem da lista l. Para ele, s
ao construdos todos os pares possveis com
os 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 segundo
elemento da lista l, 2, e formam-se os pares com os elementos da lista m. Finalmente, repete-se
este processo para o terceiro elemento da lista l. Esta forma de construcao tem import
ancia
fundamental, sendo responsavel pela construcao de listas potencialmente innitas, um t
opico
descrito no pr
oximo Captulo. Neste exemplo, tambem se nota que uma expressao ZF pode n
ao
ter qualquer express
ao boolena.
No exemplo 2) deve-se notar a import
ancia 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 colocados
para que seja possvel a geracao correta dos tri
angulos retangulos.
A funcao livrosEmprestados, denida no incio deste Captulo, pode ser re-denida da
seguinte forma:
livrosEmprestados :: BancodeDados -> Pessoa -> [Livro]
livrosEmprestados db fulano = [liv | (pes, liv) <- db, pes == fulano]
80

Exerccios:
1. Re-implemente as funcoes de atualizacao do Banco de Dados para a Biblioteca, feita no
incio do Captulo, usando compreens
ao de listas, em vez de recursao explcita.
2. Como pode a funcao membro :: [Int] > Int > Bool ser denida usando compreensao de listas e um teste de igualdade?
Exemplo. Vamos agora mostrar um exemplo mais completo, baseado na referencia [35], que
mostra algumas das possibilidades que as compreensoes oferecem. Seja um processador de texto
simples que organiza um texto, identando-o pela esquerda, como por exemplo:
Maria
gostava de bananas e
estava apaixonada
por
Joaquim e
tomou
veneno para
morrer.
Este pequeno trecho deve ser transformado em um texto mais organizado, cando da seguinte
forma:
Maria gostava de bananas e estava apaixonada
por 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 em
branco dentro dela. Os espacos em branco sao denidos da seguinte forma:
espacoEmBranco :: [Char]
espacoEmBranco = [\n, \t, ]
Vamos denir a funcao pegaPalavra que, quando aplicada a uma string, retira a primeira
palavra desta string se a string n
ao iniciar com um espaco em branco. Assim pegaPalavra
bicho besta= bicho e pegaPalavra bicho= porque a string e iniciada com um
caractere em branco.
Uma denicao para ela pode ser feita, usando a funcao pertence que verica se um determinado caractere a pertence, ou n
ao, a uma string:
pertence :: Char -> [Char] -> Bool
pertence _ [ ] = False
pertence c (a:x)
|c == a
= True
|otherwise
= pertence c x
pegaPalavra :: String -> String
pegaPalavra [ ] = [ ]
pegaPalavra (a:x)
|pertence a espacoEmBranco
= [ ]
|otherwise
= a : pegaPalavra x
J
a a funcao tiraPalavra, quando aplicada a uma string, retira a primeira palavra da string
e retorna a string restante, tendo como seu primeiro caractere o espaco em branco. Assim,
tiraPalavra bicho feio= feio.
81

tiraPalavra :: String -> String


tiraPalavra [ ] = [ ]
tiraPalavra (a : x)
|pertence a espacoEmBranco
= (a : x)
|otherwise
= tiraPalavra x
necessario construir uma funcao que retire os espacos em branco da frente das palavras.
E
Esta funcao sera tiraespaco que, aplicada a uma string iniciada com um ou mais espacos em
branco, retorna outra string sem estes espacos em branco.
tiraEspaco :: String -> String
tiraEspaco [ ] = [ ]
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 n
ao
seja iniciada com espaco em branco, ent
ao:
a primeira palavra ser
a dada por pegaPalavra st,
o restante sera feito dividindo-se a string que resulta da remocao da primeira palavra e
do espaco em branco que a segue, ou seja, a nova divisao sera feita sobre tiraEspaco
(tiraPalavra st).
type Palavra = String
divideEmPalavras :: String -> [Palavra]
divideEmPalavras st = divide (tiraEspaco st)
divide :: String -> [Palavra]
divide [ ] = [ ]
divide st = (pegaPalavra st) : divide (tiraEspaco (tiraPalavra st))
Vamos acompanhar a seq
uencia de operacoes da aplicacao divideEmPalavras bicho
bom.
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 transform
a-la em uma lista de linhas, onde
cada linha e uma lista de palavras, com um tamanho m
aximo (a linha). Para isto, vamos denir
uma funcao que forme uma u
nica linha com um tamanho determinado.
82

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 disponvel for p, ela far
a parte da linha se existir vaga para ela na
linha. O tamanho de p, length p, ter
a que ser menor ou igual ao tamanho da linha (tam).
O restante da linha e construdo a partir das palavras que restam, considerando uma linha
de tamanho tam-(length p + 1).
Se a primeira palavra n
ao se ajustar, a linha tem de ser vazia.
formaLinha tam [ ] = [ ]
formaLinha tam (p:ps)
|length p <= tam = p : restoDaLinha
|otherwise = [ ]
where
novoTam = tam - (length p + 1)
restoDaLinha = formaLinha novoTam ps
Vamos acompanhar a seq
uencia 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 que
uma linha deve ter e uma lista de palavras e retorne esta lista de palavras sem a primeira linha.
Esta funcao sera deixada como exerccio, no entanto, indicamos seu tipo.
tiraLinha :: Int -> [Palavra] -> [Palavra]
Agora e necessario juntar as coisas. Vamos construir uma funcao que transforme uma lista
de palavras em uma lista de linhas. Primeiro ela forma a primeira linha, depois retira as palavras
desta 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, formando 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 uma
string identada a` esquerda. Ser
a mostrada apenas o seu tipo, deixando sua denicao como
exerccio.
83

juntaLinhas :: [Linha] -> String


Exerccios
1. De uma denicao de uma funcao juntaLinha :: Linha > String que transforma
uma linha em uma forma imprimvel. Por exemplo, juntaLinha [bicho, bom] =
bicho bom.
2. Use a funcao juntaLinha do exerccio anterior para denir uma funcao juntaLinhas ::
[Linha] > String que junta linhas separadas por \n.
3. Modique a funcao juntaLinha de forma que ela ajuste a linha ao tamanho tam, adicionando uma quantidade de espacos entre as palavras.
4. Dena uma funcao estat :: String > (Int, Int, Int) que aplicada a um texto retorna
o n
umero de caracteres, palavras e linhas do texto. O nal de uma linha e sinalizado pelo
caractere newline (\n). Dena tambem uma funcao novoestat :: String > (Int,
Int, Int) que faz a mesma estatstica sobre o texto, apos ser ajustado.
5. Dena uma funcao subst :: String > String > String > String de forma que
subst velhaSub novaSub st faca a substituicao da sub-string velhaSub pela sub-string
novaSub em st. Por exemplo, subst much tall How much is that?= How
tall is that? (Se a sub-string velhaSub n
ao ocorrer em st, o resultado deve ser st).

4.4

Fun
c
oes de alta ordem

Provavelmente, a maioria dos leitores j


a estejam familiarizados com a ideia de listas de listas,
de dar nomes a`s listas ou de funcoes que adimitem listas como seus parametros. O que talvez
pareca extranho para muitos e a ideia de listas de funcoes ou de funcoes que retornam outras
funcoes com resultados. Esta e uma caracterstica importante das linguagens funcionais, A ideia
central e a de que as funcoes sao consideradas com os mesmos direitos que qualquer outro tipo
de dado, dizendo-se, corriqueiramente, que elas s
ao cidad
as 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 mesmo
efeito da aplicacao da funcao f, duas vezes. Assim,
twice f x = f (f (x)) = (f . f) x
Como outro exemplo, vamos considerar a seguinte denicao em Haskell:
let suc
= soma 1
soma x = somax
where somax y = x + y
in suc 3
O resultado desta aplicacao e 4. O efeito de soma x e criar uma funcao chamada somax,
que adiciona x a algum n
umero natural, no caso, y. Desta forma, suc e uma funcao que adiciona
o n
umero 1 a um n
umero 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 a
parte composta apenas por funcoes da parte dos argumentos. O operador de composicao e um
tipo de funcao, cujos argumentos s
ao duas funcoes e o resultado e tambem uma funcao.
Como mais um exemplo, vamos considerar a denicao
84

let quad
quadrado x
sucessor x
compoe (f,g)
in quad 3

=
=
=
=

compoe (quadrado, sucessor)


x * x
x + 1
h where h x = f(g(x))

A resposta a esta aplicacao e 16. O interesse maior aqui esta na denicao da funcao compoe.
Ela toma um par de par
ametros, f e g (ambos funcoes) e retorna uma outra funcao, h, cujo
efeito 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 da
funcao soma ter sido declarada com apenas um par
ametro, poderamos chama-la, por
exemplo, como soma 3 4, que daria como resultado 7. Isto signica que soma 3 tem
como resultado uma outra funcao que, aplicada a 4, d
a como resultado o valor 7.
2. Um mecanismo de aplicacao parcial de funcoes. Isto quer dizer que, se uma funcao for
declarada com n par
ametros, podemos aplica-la a m destes parametros, mesmo que m
seja menor que n.
As funcoes de alta ordem sao usadas de forma intensa em programas funcionais, permitindo
que computacoes complexas sejam expressas de forma simples. Vejamos algumas destas funcoes,
muito utilizadas na pr
atica da programacao funcional.

4.4.1

A fun
c
ao map

Um padr
ao de computacao que e explorado como funcao de alta ordem envolve a criacao de uma
lista (listaNova) a partir de uma outra lista (listaVelha), onde cada elemento de listaNova
tem 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, em
que cada nome da primeira lista e transformado em uma tupla, onde o primeiro elemento seja o
pr
oprio nome e o segundo seja a quantidade de caracteres do nome. Para isso, ser
a construda
uma 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 tupla
formada pelo nome e a quantidade de caracteres do nome.
tuplaNum :: [Char] -> ([Char], Int)
tuplaNum n = (n, length n)
Agora a funcao listaTupla pode ser denida 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 de
inteiros, onde cada valor inteiro da primeira lista seja transformado em seu dobro, ou seja,
dobraLista [3, 2, 5] = [6, 4, 10]
85

Como na denicao da funcao anterior, vamos construir a funcao auxiliar, dobra, da seguinte
forma:
dobra :: Int -> Int
dobra x = 2*x
dobraLista :: [Int] -> [Int]
dobraLista [ ]
= [ ]
dobraLista (a : x) = (dobra a) : dobraLista x
Analisando as denicoes listaTupla e dobraLista, observamos a presenca de um padr
ao
que e o de aplicar uma funcao a cada elemento da lista, ou seja, as duas funcoes percorrem as
listas aplicando uma funcao a cada um de seus elementos.
Uma outra opcao e construir uma funcao de alta ordem, destinada a realizar esta varredura, aplicando uma funcao a cada elemento da lista. A funcao a ser aplicada e passada como
par
ametro para a funcao de alta ordem. Neste caso, a funcao de alta ordem e chamada de mapeamento, simbolizado pela funcao pre-denida map, denida em Haskell da seguinte forma:
map :: (t -> u) -> [t] -> [u]
map f [ ]
= [ ]
map f (a : x) = (f a) : (map f x)
Assim, as denicoes anteriores de listaTupla e dobraLista podem ser re-denidas da seguinte 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 -> Int
duplica n = 2 * n
triplica n = 3 * n
duplicaLista, triplicaLista :: [Int] -> [Int]
duplicaLista l = map duplica l
triplicaLista 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 justica, baseando-se nas seguintes premissas:


86

mais f
E
acil entender a denicao, porque torna claro que se trata de um mapeamento, por
causa da funcao map. Precisa apenas entender a funcao mapeada.
mais f
E
acil modicar as denicoes das funcoes a serem aplicadas, se isto for necessario.
mais f
E
acil reutilizar as denicoes.
Exemplo: as funcoes de analise de vendas, mostradas no Captulo anterior, foram denidas
para analisar uma funcao xa: vendas. Agora, a funcao totalVendas pode ser dada por
totalVendas n = map vendas n
Muitas outras funcoes podem ser denidas usando map. Por exemplo,
somaQuad :: Int -> Int
somaQuad n = map quad n
quad :: Int -> Int
quad x = x * x
Exerccios
1. De denicoes 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
verique se todos os elementos da lista sao positivos.
2. Escreva denicoes de funcoes que
de o valor mnimo 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 a
zero e
teste se os valores f 0, f 1 ate f n estao em ordem crescente.
3. Estabeleca o tipo e dena uma funcao trwice que toma uma funcao de inteiros para
inteiros e um inteiro e retorna a funcao aplicada `a entrada tres vezes. Por exemplo, com
a funcao triplica e o inteiro 4 como entradas, o resultado e 108.
4. De o tipo e dena 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, dena uma funcao que aplicada a n retorne 2n .
87

4.4.2

Fun
c
oes an
onimas

J
a foi visto, e de forma ent
atica, que, nas linguagens funcionais, as funcoes podem ser usadas
como parametros para outras funcoes. No entanto, seria um desperdcio denir uma funcao que
so pudesse ser utilizada como par
ametro para outra funcao. Isto implicaria que a funcao so
fosse utilizada neste caso e nem um outro mais. No caso da secao anterior, as funcoes dobra
e tuplaNum, possivelmente, so sejam utilizadas como argumentos das funcoes dobraLista e
listaTupla.
Uma forma de declarar funcoes para serem utilizadas apenas localmente e usar a clausula
where. Por exemplo, dado um inteiro n, vamos denir uma funcao que retorne uma outra
funcao 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 especicar um valor, normalmente, se declara um identicador para isto.
Esta tambem tem sido a forma utilizada com as funcoes, ou seja, declara-se uma funcao com
um nome e, quando necessaria, e referenciada atraves de seu nome. Uma alternativa, possvel
em Haskell, consiste em declarar uma funcao apenas no ponto de chamada. Estas funcoes sao
as func
oes an
onimas. As funcoes anonimas se baseiam na notacao do -calculo, visto no
Captulo 2. Esta forma e mais compacta e mais eciente. A funcao somaNum, denida acima
usando a cl
ausula where, pode ser escrita, de forma anonima, da seguinte forma:
\m -> n + m
As funcoes anonimas permitem um acesso direto a funcoes, sem identicadores. Nese caso,
as funcoes dobraLista e listaTupla podem ser denidas da seguinte forma:
dobraLista x = map (\n -> 2*n) x
listaTupla x = map (\n -> (n, length n)) x
Vamos agora analisar como a sintaxe de uma funcao an
onima e feita. Uma denicao an
onima
e dividida em duas partes: uma antes da exa e a outra depois dela. Estas duas partes tem as
seguintes interpretacoes:
antes da exa vem os argumentos (neste caso, apenas n) e
depois da exa vem o resultado.
A barra invertida no incio (\) indica que se trata de uma funcao an
onima. A \ e o caractere
mais parecido com a letra grega , usada no -calculo.
Vejamos a funcao comp2, mostrada gracamente na Figura 4.1. Na realidade, trata-se de
uma funcao g que recebe como entrada dois argumentos, no caso f x e f y, que s
ao os resultados
das aplicacoes da funcao f aos argumentos x e y, ou seja g (f x) (f y).
A denicao 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

comp2 f g
x

f
g (f x) (f y)

Figura 4.1: Forma gr


aca da funcao an
onima comp2.

onde quad e soma tem signicados obvios. De forma geral, sendo f denida por
f x y z = resultado
entao f pode ser denida anonimamente por
\x y z -> resultado
As fun
c
oes fold e foldr
Uma outra funcao de alta ordem, tambem de grande utilizacao em aplicacoes funcionais, e a
funcao fold, usada na combinacao de tens (folding). Ela toma como argumentos uma funcao
de dois argumentos e a aplica aos elementos de uma lista. O resultado e um elemento do tipo
dos elementos da lista. Vamos ver sua denicao formal e exemplos de sua aplicacao.
fold :: (t -> t -> t) -> [t] -> t
fold f [a] = a
fold f (a:b:x) = f a (fold f (b:x))
Exemplos:
fold (||) [False, True, False]

fold (++) [Chico, Afonso, , !]


fold (*) [1..6]

=
=
=
=
=
=
=

(||) False (fold (||) [True, False])


(||) False ((||) True (fold (||) [False])
(||) False ((||) True False)
(||) False True
True
Chico Afonso!
720 (Verique!)

No entanto, existe um pequeno problema na denicao de fold. Se ela for aplicada a uma
funcao e uma lista vazia ocorrera um erro. Para resolver este impasse, foi pre-denida, em
Haskell, uma outra funcao para substituir fold, onde este tipo de erro seja resolvido. Esta e a
funcao foldr, denida da seguinte forma:
foldr :: (t -> u -> u) -> u -> [t] -> u
foldr f s [ ] = s
foldr f s (a : x) = f a (foldr f s x)
Vamos vericar como algumas funcoes sao denidas usando foldr.
Exemplos:
89

concat :: [[t]] -> [t]


concat xs = foldr (++) [ ] xs

and :: [Bool] -> Bool


and bs = foldr (&&) True bs

rev :: [t] -> [t]


rev l = foldr stick [] l

stick :: t -> [t] -> [t]


stick a x = x ++ [a]

A fun
c
ao filter
A funcao filter e uma outra funcao de alta ordem, pre-denida em todas as linguagens funcionais,
de bastante utilizacao. Resumidamente, ela escolhe dentre os elementos de uma lista, aqueles
que tem uma determinada propriedade. Vejamos alguns exemplos:
1. filter ehPar [2,3,4] = [2,4].
2. Um n
umero natural e perfeito se a soma de seus divisores, incluindo o n
umero 1, for o
pr
oprio n
umero. Por exemplo, 6 e o primeiro n
umero natural perfeito, porque 6 = 1+2+3.
Vamos denir uma funcao que mostra os n
umeros perfeitos entre 0 e m.
divide :: Int -> Int -> Bool
divide n a = n mod a == 0
fatores :: Int -> [Int]
fatores n = filter (divide n) [1..(n div 2)]
perfeito :: Int -> Bool
perfeito n = sum (fatores n) == n
perfeitos m = filter perfeito [0..m]
E se quizermos os primeiros m n
umeros perfeitos?
3. A lista de todos os n
umeros pares maiores que 113 e menores ou iguais a 1000, que sejam
perfeitos: filter perfeito [y | y < [114 .. 1000], ehPar y].
4. Selecionando elementos: filter digits 18 Marco 1958 = 181958
A denicao formal de filter e:
filter :: (t -> Bool) -> [t] -> [t]
filter p [ ] = [ ]
filter p (a : x)
ou
|p a
= a : filter p x
|otherwise = filter p x

4.5

filter p x = [a | a <- x, p a]

Polimorfismo

Uma caracterstica muito importante das linguagens funcionais e que suas denicoes podem
ser polimorcas, um mecanismo que aumenta o poder de expressividade de qualquer linguagem. Polimorsmo e uma das caractersticas responsaveis pela alta produtividade de software,
proporcionada pelo aumento da reusabilidade.
90

Polimorsmo e a capacidade de aplicar uma mesma funcao a v


arios tipos de dados, representados por um tipo vari
avel. Nao deve ser confundido com sobrecarga, denominada por muitos
pesquisadores como polimorsmo ad hoc, que consiste na aplicacao de v
arias funcoes com o
mesmo nome a varios tipos de dados. Haskell permite os dois tipos de polimorsmo, sendo que
a sobrecarga e feita atraves de um mecanismo engenhoso chamado de type class, um tema a
ser estudado no pr
oximo Captulo.
A funcao length e pre-denida em Haskell, da seguinte maneira:
length :: [t] -> Int
length [ ] = 0
length (a : x) = 1 + length x
Esta funcao tem um tipo polim
orfico porque pode ser aplicada a qualquer tipo de lista
homogenea. Em sua denicao n
ao existe qualquer operacao que exija que a lista par
ametro seja
de algum tipo particular. A u
nica operacao que esta funcao faz e contar os elementos de uma
lista, seja ela de que tipo for.
J
a a funcao
quadrado :: Int -> Int
quadrado x = x * x
n
ao pode ser polim
orca porque s
o e aplic
avel a elementos onde a operacao de multiplicacao (*)
seja possvel. Por exemplo, nao pode ser aplicada a strings, nem a valores booleanos.

4.5.1

Tipos vari
aveis

Quando uma funcao tem um tipo envolvendo um ou mais tipos vari


aveis, diz-se que ela tem
um tipo polim
orco. Por exemplo, j
a vimos anteriormente que a lista vazia e um elemento de
qualquer tipo de lista, ou seja, [ ] est
a na intersecao dos tipos [Int], [Bool] ou [Char], etc. Para
explicitar esta caracterstica denota-se que [ ] :: [t], sendo t e uma vari
avel que pode assumir
qualquer tipo. Assim, cons tem o tipo polim
orco (:) :: t > [t] > [t].
As seguintes funcoes, algumas j
a denidas anteriormente, tem os tipos:
length :: [t] -> Int
(++) :: [t] -> [t] -> [t]
rev :: [t] -> [t]
id :: t -> t
zip :: [t] -> [u] -> [(t, u)]

4.5.2

O tipo mais geral

Alguma diculdade pode surgir nas denicoes dos tipos das funcoes quando elas envolvem tipos
vari
aveis, uma vez que podem existir muitas instancias de um tipo vari
avel. Para resolver este
dilema, e necessario que o tipo da funcao seja o tipo mais geral possvel.
Um tipo w de uma funcao f e o tipo mais geral de f se todos os tipos de f forem inst
ancias
de w.
O tipo [t] > [t] > [(t, t)] e uma inst
ancia do tipo da funcao zip, mas nao e o tipo
mais geral, porque [Int] > [Bool] > [(Int, Bool)] e um tipo para zip, mas nao e uma
instancia de [t] > [t] > [(t, t)].
Exemplos de algumas fun
c
oes polim
orficas.
91

rep :: Int -> t -> [t]


rep 0 ch = [ ]
rep n ch = ch : rep (n - 1) ch
fst :: (t, u) -> t
fst (x, _) = x

snd :: (t, u) -> u


snd (_, y) = y

head :: [t] -> t


head (a : _) = a

tail :: [t] -> [t]


tail (_ : x) = x

Mas qual a vantagem de se ter polimorsmo? A resposta vem da Engenharia de Software e


se baseia nos seguintes fatos:
denicoes mais gerais implicam em maior chance de reutilizacao e
em linguagens n
ao polimorcas, as funcoes devem ser re-denidas para cada novo tipo.
Isto implica em ineciencia e inseguranca.
Exerccios
1. Dena uma funcao concat onde concat [e1 , ..., ek ] = e1 ++ ... ++ ek . Qual o tipo
de concat?
2. Dena uma funcao unZip que transforma uma lista de pares em um par de listas. Qual
o seu tipo?
3. Dena uma funcao last :: [t] > t que retorna o u
ltimo elemento de uma lista n
ao
vazia. Dena tambem init :: [t] > [t] que retorna todos os elementos de uma lista com
excecao do u
ltimo elemento da lista.
4. Dena funcoes tome, tire :: Int > [t] > [t] onde tome n l retorna os n primeiros
elementos da lista l e tire n l retira os n primeiros elementos da lista l.

4.6

Induc
ao estrutural

J
a vimos formas de se provar propriedades em Haskell. No entanto, quando estas propriedades
envolvem listas, existe uma forma especca de serem provadas, que e a inducao estrutural. Podese dizer que inducao estrutural e o metodo de inducao matematica aplicado `as listas nitas. As
listas innitas n
ao sao tratadas, uma vez que elas nao sao estruturas model
aveis na Computacao.
Os computadores sao maquinas com memorias limitadas, apesar de poderem ser grandes, mas
sao nitas. Apesar de alguns autores se referirem a`s listas innitas, o que realmente eles se
referem sao as listas potencialmente innitas, que s
ao implementadas em Haskell atraves de
um mecanismo de avaliacao lazy, um t
opico a ser visto mais adiante. Deve ser lembrado que a
lista vazia, [ ], e uma lista nita e a lista nao vazia, (a:x), e uma lista nita, se a lista x for
nita.
Esquema de prova
O esquema de provas mostrado a seguir e devido a Simon Thompson [35]. Apesar de muito
formal, ele deve ser seguido, principalmente, por iniciantes, que ainda n
ao tem experiencia com
provas de programas. Para estes usu
arios, e recomendavel utiliza-lo como forma de treinamento.
92

Muitas pessoas tendem a querer chegar `a conclusao de uma prova forcando situacoes, sem a
argumentacao adequada e este tipo de vcio h
a que ser evitado, a qualquer custo. A falta de
domnio nesta area pode levar o usu
ario a conclusoes equivocadas.
Outro erro, comumente cometido por algumas pessoas n
ao afeitas a provas matematicas,
consiste em realizar provas sem se importar com as conclusoes das mesmas. A conclus
ao e parte
ntegrante do esquema de provas e, portanto, indispens
avel. Ela e o objetivo da prova. Sem
ela n
ao existe razao para todo um esforco a ser despendido nas fases anteriores. A conclus
ao
representa o desfecho de uma prova e representa a formalizacao de uma proposicao que passa a
ser verdadeira e pode ser utilizada em qualquer etapa de uma computacao.
O esquema de provas deve se constituir nos seguintes estagios:
Est
agio 0:
Est
agio 1:
Est
agio 2:

Est
agio 3:
Est
agio 4:

escrever o objetivo da prova informalmente,


escrever o objetivo da prova formalmente,
escrever os sub-objetivos da prova por inducao:
P([ ]) e
P(a : x), assumindo P(x)
provar P([ ])
provar P(a : x), lembrando que PODE e DEVE usar P(x).

Vejamos agora, dois exemplos completos de esquemas de provas que envolvem todos os
estagios enumerados anteriormente.
Exemplo 1. Dadas as denicoes a seguir:
somaLista [ ] = 0
somaLista (a : x) = a + somaLista x

(1)
(2)

dobra [ ] = [ ]
dobra (a : x) = (2 * a) : dobra x

(3)
(4)

Provar que
Est
agio 0: o dobro da soma dos elementos de uma lista e igual a` soma dos elementos da
lista formada pelos dobros dos elementos da lista anterior.
Est
agio 1: somaLista (dobra x) = 2 * somaLista x

(5)

Est
agio 2:
sumList (dobra [ ]) = 2 * somaLista [ ] (6)
somaLista (dobra (a : x)) = 2 * somaLista (a : x) (7)
assumindo que somaLista (dobra x) = 2 * somaLista x (8)
Est
agio 3: Caso base:
lado esquerdo do caso base:
somaLista (dobra [ ])
= somaLista [ ]
=0

por (3)
por (1)

lado direito do caso base


2 * somaLista [ ]
=2 * 0
=0

Assim, a assertiva (6) e valida.


Est
agio 4: Passo indutivo:
93

por (1)
pela aritmetica.

lado esquerdo do passo indutivo:


somaLista (dobra (a : x))
= somaLista (2 * a : dobra x)
= 2 * a + somaLista (dobra x)
= 2 * a + 2 * somaLista x
= 2 * (a + somaLista x)
lado direito do passo indutivo:
2 * somaLista (a : x)
= 2 * (a + somaLista x)

por (4)
por (2)
pela hip
otese de inducao
pela distributividade de *.

por (2).

Assim, a assertiva (7) e valida.


Conclus
ao: como a assertiva (5) e valida para o caso base e para o passo indutivo, ent
ao ela e
v
alida para todas as listas nitas.
Exemplo 2. Associatividade de append.
Est
agio 0: a funcao ++ e associativa.
Est
agio 1: Dadas as denicoes:
[ ] ++ v = v
(1)
(a : x) ++ v = a : (x ++ v)

(2)

x ++ (y ++ z) = (x ++ y) ++ z
Est
agio 2:
caso base: [ ] ++ (y ++ z) = ([ ] ++ y) ++ z (3)
passo indutivo: (a : x) ++ (y ++ z) = ((a : x) ++ y) ++ z (4) assumindo
que x ++ (y ++ z) = (x ++ y) ++ z (5)
Est
agio 3:
caso base: lado esquerdo
[ ] ++ (y ++ z)
= y ++ z

por (1)

caso base: lado direito


([ ] ++ y) ++ z
= y ++ z

por (1)

Como o lado esquerdo e o lado direito do caso base s


ao iguais, entao a propriedade e valida
para ele.
Est
agio 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
= a : ((x ++ y) ++ z)
= a : (x ++ (y ++ z))

por (2)
por (2)
pela hip
otese de inducao.

Como o lado esquerdo e o lado direito do passo indutivo s


ao iguais, entao a propriedade e
v
alida para ele.
94

Conclus
ao: como a assertiva e valida para o caso base e para o passo indutivo, ent
ao ela e
verdadeira para todas as listas nitas.
Exerccios
1. Prove que, para todas as listas nitas x, x ++ [ ] = x.
2. Tente provar que x ++ (y ++ z) = (x ++ y) ++ z usando inducao estrutural sobre
z. H
a alguma coisa esquisita com esta prova? O que?
3. Prove que, para todas as listas nitas 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 nitas x e y,
double (x ++ y) = double x ++ double y e
length (x ++ y) = length x + length y.
5. Prove por inducao sobre x que, para todas as listas nitas x,
sumList (x ++ (a : y)) = a + sumList (x ++ y).
6. Prove, usando ou n
ao o exerccio anterior, que para todas as listas nitas x,
sumList (double x) = sumList (x ++ x).

4.7

Composi
c
ao de fun
c
oes

Uma forma simples de estruturar um programa e constru-lo em etapas, uma apos outra, onde
cada uma delas pode ser denida separadamente. Em programacao funcional, isto e feito atraves
da composicao de funcoes, uma propriedade matematica so implementada nestas linguagens. A
sua sintaxe obedece aos princpios b
asicos empregados na matematica e aumenta, enormemente,
a expressividade do programador. A funcao preenche foi denida anteriormente da seguinte
forma:
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 codicacao torna a composicao explcita sem a necessidade de aplicar cada
lado da igualdade a um argumento. Da Matem
atica herdamos a notacao de ., onde
(f . g) x = f ( g x )
Como e sabido da Matem
atica, nem todo par de funcoes pode ser composto. O tipo da sada
da 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 interpretado
como faca h, depois faca g e nalmente faca f .
95

4.7.1

Composi
c
ao avan
cada

A ordem em f . g e importante (faca g e depois f ). Podemos fazer a composicao ter o sentido


das acoes propostas, usando a composicao avancada. Deve ser lembrado que esta forma e
apenas para ns de apresentacao, uma vez que a denicao, como sera vista, e feita em funcao
da composicao e nada aumenta em expressividade. A composicao avancada sera denotada por
> . > indicando a sequencializacao da aplicacao das funcoes, uma vez que, na composicao, ela
e realizada na ordem inversa de aparencia no texto. A denicao de > . > e feita da seguinte
forma:
infixl 9 >.>
(>.>) :: (t -> u) -> (u -> v) -> (t -> v)
g >.> f = f . g
A funcao preenche, denida anteriormente, pode ser re-denida 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 aplicacao
de funcoes tem prioridade sobre a composicao. Por exemplo, succ . succ 1 resultar
a em erro
porque succ 1 sera realizada primeiro e retornar
a um inteiro (2), fazendo com que a composicao
do primeiro succ seja feita com um n
umero inteiro e n
ao com uma funcao. Neste caso, os
parenteses devem ser utilizados para resolver ambiguidades.

4.7.2

Esquema de provas usando composi


c
ao

Seja a funcao twice f = f . f. Assim


(twice succ) 12
= (succ . succ) 12
= succ (succ 12)
= succ 13 = 14

--pela definicao de twice


--pela definicao de .

Pode-se generalizar twice, indicando um par


ametro que informe quantas vezes a funcao deve
ser composta com ela propria:
iter :: Int -> (t -> t) -> (t -> t)
iter 0 f = id
iter n f = f >.> iter (n - 1) f
uencia de
Por exemplo, podemos denir 2n como iter n duplica e vamos mostrar a seq
operacoes para n=2.
iter 2 duplica

=
=
=
=
=

duplica >.> iter 1 duplica


duplica >.> (duplica >.> iter 0 f)
duplica >.> (duplica >.> id))
duplica >.> duplica
twice duplica

Nesta denicao, usamos o fato de que f . id = f. Estamos tratando de uma nova especie de
igualdade, que e a igualdade de duas funcoes. Mas como isto pode ser feito? Para isto, devemos
96

examinar como os dois lados se comportam quando os aplicamos a um mesmo argumento x.


Ent
ao,
(f . id) x
= f (id x)
=fx

pela denicao de composicao


pela denicao de id.

Isto signica que para um argumento x, qualquer, as duas funcoes se comportam exatamente
da mesma forma.
Vamos fazer uma pequena discussao sobre o que signica a igualdade entre duas funcoes.
Existem dois princpios que devem ser observados quando nos referimos a` igualdade de funcoes.
S
ao eles:
Princpio da extensionalidade: Duas funcoes, f e g, sao iguais se elas produzirem
exatamente os mesmos resultados para os mesmos argumentos.
Princpio da intencionalidade: Duas funcoes, f e g sao iguais se tiverem as mesmas
denicoes.
Se estivermos interessados nos resultados de nossos programas, tudo o que nos interessa
sao 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 eciencia ou outros aspectos de desempenho de programas devemos
usar a intencionalidade.
Exerccios
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 curry
e uncurry, denidas a seguir, s
ao 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 denicao de map e da composicao de funcoes dadas a seguir. Mostre que
map (f. g) x = (map f . map g) x
map f [ ] = [ ]
map f (a : x) = f a : map f x
(f . g) x = f (g x)

(1)
(2)
(3)

Prova:
Caso base: a lista vazia, [ ]:
Lado esquerdo
map (f . g) [ ] = [ ]

por (1)

Lado direito
(map f . map g) [ ]
= map f (map g [ ])
=map f [ ]
=[ ]
97

por (3)
por (1)
por (1)

Passo indutivo: (a : x)
Lado esquerdo
map (f . g) (a : x)
=(f . g) a : map (f . g) x
=f (g a) : map (f . g) x
=f (g a) : (map f . map g) x

(2)
(3)
(hi)

Lado direito
(map f . map g)(a:x)
= map f (map g (a:x))
=map f (g a) : map f (map g x)
=f (g a) : (map f . map g) x

(3)
(2)
(3)

Conclus
ao. Como a propriedade e valida para a lista vazia e para a lista nao vazia, entao ela
e valida para qualquer lista homogenea nita.
Exerccio: Prove que para todas as listas nitas l e funcoes f, concat (map (map f ) l) =
map f (concat l).

4.8

Aplica
c
ao parcial

Uma caracterstica importante de Haskell e que proporciona uma forma elegante e poderosa de
construcao de funcoes e a avaliacao parcial que consiste na aplicacao de uma funcao a menos
argumentos que ela realmente precisa. Por exemplo, seja a funcao multiplica que retorna o
produto de seus argumentos:
multiplica :: Int -> Int -> Int
multiplica a b = a * b
Esta funcao foi declarada para ser usada com dois argumentos. No entanto, ela pode ser
chamada como multiplica 2. Esta aplicacao retorna uma outra funcao que, aplicada a um
argumento b, retorna o valor 2*b. Esta caracterstica e o resultado do seguinte princpio em
Haskell: uma func
ao com n argumentos pode ser aplicada a r argumentos, onde r n. Como
exemplo, a funcao dobraLista pode ser denida 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 de
seus argumentos, 2, em vez de ser aplicada aos dois. map (multiplica 2) e uma funcao do
tipo [Int] > [Int], dada pela aplicacao parcial de map.
Como e determinado o tipo de uma aplicacao parcial? Pela regra do cancelamento: se uma
funcao f tem o tipo t1 > t2 > ... > tn > t e e aplicada aos argumentos e1 :: t1 , e2:: t2 ,
ao o tipo do resultado e dado pelo cancelamento dos tipos t1 at
e
... , ek :: tk , com k n, ent
tk , dando o tipo tk+1 > tk+2 > ... tn t.
Por exemplo,
multiplica
multiplica
dobraLista
dobraLista

2 :: Int -> Int


2 3 :: Int
:: [Int] -> [Int]
[2,3,5] :: [Int]

Mas anal, quantos argumentos tem realmente uma funcao em Haskell? Pelo exposto, a
resposta correta a esta questao e 1 (UM).
Isto signica que uma funcao do tipo Int > Int > Int e do mesmo tipo que Int
> (Int > Int). Neste u
ltimo caso, esta explcito que esta funcao pode ser aplicada a um
argumento inteiro e o seu resultado e uma outra funcao que recebe um inteiro e retorna outro
inteiro. Exemplicando,
98

multiplica
:: Int -> Int -> Int
multiplica 4 :: Int -> Int
multiply 4 5 :: Int

ou

multiplica :: Int -> (Int -> Int)

Associatividade:
A aplicacao de funcao e associativa `a esquerda, ou seja:
f a b = (f a) b
enquanto a exa e associativa pela direita:
t > u > v = t > (u > v).

4.8.1

Se
c
ao de operadores

Uma decorrencia direta das aplicacoes parciais que representa uma ferramenta poderosa e elegante em algumas linguagens funcionais e, em particular, em Haskell, s
ao as secoes de operadores.
As secoes sao operacoes parciais, normalmente relacionadas com as operacoes aritmeticas. Nas
aplicacoes, 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 nal de uma string.
Uma secao de um operador op coloca o argumento no lado que completa a aplicacao. Por
exemplo, (op a) b = b op a e (a op) b = a op b.
Exemplos:
map (+1) > . > filter ( >0).
dobra = map (*2).
pegaPares = filter (( ==0) . (mod2)).
A funcao inquilinos, denida no incio deste Captulo, normalmente, e escrita da seguinte
forma:
inquilinos db pes = map snd (filter ehPes db)
where
ehPes (p, b) = (p == pes).
No entanto, ela pode ser escrita em forma de secao da seguinte maneira, o que a torna
muito mais elegante.
inquilinos db pes = map snd (filter ((==pes) . fst) db).

4.8.2

Currificac
ao

Este nome tambem foi cunhado em homenagem a Haskell Brooks Curry, por sua pesquisa na
l
ogica e no -calculo. Na realidade, Sch
onnkel foi o pioneiro, mas Haskell foi quem mais
99

utilizou esta propriedade em suas pesquisas: nas linguagens funcionais, uma func
ao de aridade
n e equivalente a n func
oes de aridade 1.
Exemplo.
Este e mais um exemplo que mostra o poder de expressividade de Haskell. Trata-se da criacao
de um ndice remissivo, encontrado na maioria dos livros tecnicos. Este exemplo e baseado no
livro de Simon Thompson [35]. Algumas funcoes ja foram denidas anteriormente, no entanto
elas serao novamente denidas aqui para evitar ambig
uidades. Vamos mostrar, inicialmente, os
tipos de dados utilizados na simulacao.
type Doc = String
type Linha = String
type Palavra = String
fazIndice :: Doc -> [ ([Int], Palavra) ]
Neste caso, o texto e uma string como a seguinte:
doc :: Doc
doc = "Imagine theres no heaven\nIts easy if you try\nNo hell
below us\nAbove us only sky\nImagine all the people\nliving
for today"
Vamos utilizar este trecho e mostrar como ele vai ser transformado com a aplicacao das
funcoes que serao denidas para realizar operacoes sobre ele:
Dividir doc em linhas, cando assim:
[Imagine theres no heaven,
Its 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 n
umero de linha:
[(1, Imagine theres no heaven),
(2, Its 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 n
umero da
linha onde ela ocorre
[(1, Imagine), (1, theres), (1, no), (1, heaven),
(2, Its), (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

Agora e necessario ordenar esta lista em ordem alfabetica das palavras e, se a mesma
palavra ocorre em mais de uma linha, ordenar por linha.
[(4, Above), (5, all), (3, below), (2, easy), (6, for), (1, heaven), (3, hell),
(2, Its), (5, Imagine), (2, if), (6, Living), (3, No), (1, no),
(4, only), (5, people), (4, sky), (5, the), (1, theres), (6, today),
(2, try), (3, us), (4, us), (2, you)]
Esta operacao e realizada por ordenaLista :: [(Int, Palavra)] > [(Int, Palavra)]
Agora temos que modicar a lista de forma que cada palavra seja emparelhada com a lista
unit
aria das linhas onde ela ocorre:
[([4], Above), ([5], all), ([3], below), ([2], easy), ([6], for), ([1], heaven),
([3], hell), ([2], Its), ([5], Imagine), ([2], if), ([6], Living), ([3], No),
([1], no), ([4], only), ([5], people), ([4], sky), ([5], the), ([1], theres),
([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 de
linhas
[([4], Above), ([5], all), ([3], below), ([2], easy), ([6], for), ([1], heaven),
([3], hell), ([2], Its), ([5], Imagine), ([2], if), ([6], Living), ([3], No),
([1], no), ([4], only), ([5], people), ([4], sky), ([5], the), ([1], theres),
([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 de
4 letras
[([4], Above), ([3], below), ([2], easy), ([1], heaven), ([3], hell),
([2], Its), ([5], Imagine), ([6], Living),
([4], only), ([5], people), ([1], theres), ([6], today)]
Esta operacao e realizada por diminui :: [([Int], Palavra)] > [([Int], Palavra)]
Usando composicao avancada, para car 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 -> Doc
tiraEspaco [ ] = [ ]
tiraEspaco (a : x)
|a == = tiraEspaco x
|otherwise = (a : x)
pegaLinha :: Doc -> [Linha]
pegaLinha [ ] = [ ]
101

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 -> String
tiraPalavra [ ] = [ ]
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 denir a funcao de ordenacao usando quicksort:
compara :: (Int, Palavra) -> (Int, Palavra) -> Bool
compara (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

|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 n
ao foram projetadas apenas para
fazer calculos matematicos, como fibonacci e fatorial. Na realidade, Haskell serve tambem
para estes calculos, mas sua area de aplicacao e muito mais ampla do que e imaginada por
alguns programadores, defensores intransigentes de outros paradigmas de programacao. O leitor
deve reler o Captulo introdut
orio deste estudo e consultar as referencias [37, 15] para maiores
informacoes sobre este tema ou consultar o site ocial de Haskell.

4.9

Melhorando o desempenho de uma implementa


c
ao

Ate agora mostramos ao leitor formas de denir funcoes em Haskell sem nos preocupar com o
desempenho de cada implementacao. Nosso objetivo tem sido apenas construir denicoes, de
forma que elas funcionem. No entanto, este foi um objetivo inicial, uma vez que a preocupacao
com desempenho deve ser uma constante em qualquer programador. Nesta secao, vamos fazer
algumas observacoes relacionadas ao desempenho de funcoes, dando os primeiros passos nesta
direcao. Vamos iniciar com a analise do desempenho da funcao reverse, muito utilizada nas
denicoes de funcoes sobre listas.

4.9.1

O desempenho da fun
c
ao reverse

A funcao reverse inverte a ordem dos elementos de uma lista de qualquer tipo. Ela e pre-denida
em Haskell, e poderia ser facilmente denida por
reverse :: [t] -> [t]
reverse [ ] = [ ]
reverse (a : x) = reverse x ++ [a]
A despeito da simplicidade desta denicao, ela padece de um serio problema de desempenho.
Vamos vericar quantos passos seriam necessarios para inverter uma lista l de n elementos,
usando este denicao. 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 constante
para suas execucoes. A operacao ++ deve saltar para o nal de seu primeiro argumento para
concaten
a-lo com o segundo. O tempo total de reverse e dado pela soma
(n 1) + (n 2) + (n 3) + . . . + 1 + 0 =

n1

i=0

i=

n(n 1)
O(n2 )
2

Assim, reverse precisa de um tempo proporcional ao quadrado do tamanho da lista a ser


invertida. Ser
a que existe outra forma de implementar reverse com um desempenho melhor?
A resposta e sim, ou seja, e possvel implementa-la em um tempo proporcional ao tamanho da
lista. Vejamos como isto pode ser feito.
Imagine inverter a ordem de uma pilha de livros. A ideia e retirar o livro que se encontra no
topo da pilha e coloc
a-lo ao lado, iniciando uma nova pilha de livros. Em seguida prosseguimos
com o processo de retirar mais um livro do topo da pilha anterior e coloc
a-lo no topo da nova
103

pilha, ate que a pilha anterior que vazia. Neste ponto, a nova pilha ser
a a pilha anterior em
ordem inversa.
Vamos aplicar este mesmo raciocnio na implementacao da funcao reverse. Para isto usaremos duas listas, pl e sl, para simular as pilhas de livros. Vamos denir uma funcao auxiliar,
revaux que, aplicada `a duas listas, retira o elemento da cabeca da primeira, pl, e o coloca como
cabeca da segunda, sl. A funcao termina quando pl car vazia, e o resultado sera a lista sl.
Formalmente esta denicao 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 an
alise da complexidade desta denicao nos informa que ela e proporcional ao tamanho
da 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 an


alise acurada de
sua eciencia e impossvel ser feita porque os tamanhos das sub-listas menores e maiores n
ao
podem ser pre-determinados. No entanto, verica-se que o pior caso para esta funcao acontece
quando 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 completamente
ordenada. Neste caso, a analise de tempo se torna
TqsortW C (n) =

n2
2

5n
2

sendo n o tamanho da lista [30]. Isto signica que a complexidade de tempo para o pior caso e
O(n2 ). Esta complexidade e pior que a grande maioria dos algoritmos de ordenacao, apesar de
ser um caso pouco provavel de acontecer. Um caso mais realista consiste na hipotese de que a
lista original seja dividida em duas sub-listas de tamanhos pr
oximos. Neste caso, a analise da
eciencia 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 complexidade
para o pior caso.
A an
alise da eciencia de espaco acumulado nos leva a resultados similares, ou seja
SqsortW C (n) =

n2
2

n
2

SqsortAC = n log n + n
Isto mostra que a complexidade de espaco do quicksort para o caso medio tambem e
O(n log n).
Para nalizar este Captulo, vamos colocar uma lista de exerccios sobre listas. Estes
exerccios devem ser resolvidos pelo leitor para adquirir domnio sobre a construcao de listas
para as diversas nalidades.
Exerccios.
1. Dena, em Haskell, uma funcao f que, dadas uma lista i de inteiros e uma lista l qualquer,
retorne uma nova lista constituda pela lista l seguida de seus elementos que tem posicao
104

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 compreens
ao, dena uma funcao em Haskell, que gere todas as tuplas ordenadas
de n
umeros x, y, e z menores ou iguais a um dado n
umero n, tal que x2 + y 2 = z 2 .
3. Dena, em Haskell, uma funcao que calcule o Determinante de uma matriz quadrada de
ordem n.
4. Encontre todas as solucoes possveis para se colocar 8 rainhas em um tabuleiro de xadrez
de forma que nenhuma delas ataque qualquer uma outra.
5. Dena, 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 elementos
de 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 lquido (LCD) contem uma matriz 5x3 que pode mostrar um
n
umero, como 9 e 5, por exemplo:
***
* *
***
*
*

***
*
***
*
***

O formato de cada n
umero e dinido por uma lista de inteiros que indicam quantos * se
repetem, seguidos de quantos brancos se repetem, ate o nal da matriz 5x3, comecando
da primeira linha ate a u
ltima:
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 n
umero 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 n
umero (lista de inteiros) gerar a String correspondente de * e
espacos.
toString :: [Int] -> String
toString 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

type Linha = String


toLinhas :: String -> [Linha]
toLinhas **** **** * *
==> [***, * *,***,

*,

*]

c. Faca uma funcao que pegue uma lista de Strings e a transforme em uma u
nica
String com
n entre cada uma delas:
showLinhas :: [Linha] -> String
showLinhas [***, * *, ***,
==> ***\n* *\n***\n *\n *

*,

*]

d. Faca uma funcao que pegue duas listas de linhas e transforme-as em uma u
nica lista
de linhas, onde as linhas originais se tornam uma u
nica, com um espaco entre elas:
juntaLinhas :: [LInha] ->
juntaLInhas [***, *
[***, *
==> [*** ***,
*
*, *

[Linha] -> [Linha]


*, ***, *, *]
*, ***, *, *]
* * * *, *** ***,
*]

e. Faca uma funcao que, dado um inteiro, imprima-o usando *s, espacos e
ns. A funcao tem que funcionar para inteiros de 2 e 3 dgitos.
tolcd :: Int -> String
Dica: use as funcoes div e mod de inteiros, a funcao (!! e a lista numeros, dada a
seguir:
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 da
String 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. Dena, em Haskell, uma funcao que aplicada a uma lista l e a um inteiro n, retorne todas
as sublistas de l com comprimento maior ou igual a n.

4.10

Resumo

Este Captulo foi dedicado inteiramente ao estudo das listas em Haskell, completando o estudo
dos tipos primitivos adotados em Haskell, alem do tipo estruturado produto cartesiano, representado pelas tuplas. Ele se tornou necessario, dada a import
ancia que as listas em nas linguagens
funcionais. Foi dada enfase `as funcoes pre-denidas e a`s Compreensoes, tambem conhecidas
por express
oes ZF, mostrando a facilidade de se construir funcoes com esta ferramenta da linguagem. Vimos tambem a elegancia com que algumas funcoes, como por exemplo, o quicksort,
foram denidas. Finalmente foram vistas caractersticas importantes na construcao de funcoes
como: polimorsmo, composicao, avaliacao parcial e curricacao de funcoes.
Apesar dos tipos de dados estudados ate aqui j
a signicarem um avanco importante, Haskell
vai mais alem. A linguagem tambem permite a criacao de tipos abstratos de dados e tambem
dos tipos algebricos de dados, temas a serem estudados no proximo Captulo.
106

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 pr
atico existe relacionado com exerccios
usando listas. O livro de Paul Hudak [14] e tambem uma fonte de consulta importante, dada a
sua aplicacao a` multimdia.
Uma fonte importante de problemas que podem ser resolvidos utilizando as ferramentas aqui
mostradas e o livro de Steven S. Skiena e Miguel A. Revilla [33], que apresenta um cat
alogo dos
mais variados tipos de problemas, desde um nvel inicial ate problemas complexos e de solucao
complexa.
Outra fonte importante de problemas matem
aticos envolvendo grafos e o livro de Sriram
Pemmaraju e Steven Skiena [28]. Esta referencia implementa seus exerccios em uma ferramenta conhecida como Mathematica, e podem ser facilmente traduzidos para Haskell, dada
a proximidade sint
atica entre os programas codicados em Haskell e as solucoes adotadas na
Mathematica.

107

108

Captulo 5

Tipos de dados complexos


Languages which aim to improve
productivity must support modular programming well.
But new scope rules and mechanisms for separate
compilation are not enough-modularity means more than moduoles.
Our ability to decompose a problem into
parts 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

Introdu
c
ao

Este Captulo e dedicado a` construcao de novos tipos de dados, mais complexos que os ate agora
apresentados, que s
ao os tipos algebricos e os tipos abstratos de dados.
Vamos iniciar este estudo com as classes de tipos, conhecidas mais comumente como type
class. O domnio deste tema e necessario porque as classes de tipos representam a forma usada
em Haskell para implementar sobrecarga de operadores. Alem disso, as classes de tipos tambem
sao 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, representam
um aumento na expressividade e no poder de abstracao de Haskell. Por estes motivos, este
estudo deve ser de domnio pleno para quem deseja aproveitar as possibilidades que a linguagem
oferece. Neste Captulo, tambem e feito um estudo sobre o tratamento de erros, sobre as provas
de programas envolvendo tipos algebricos de dados e sobre os modulos em Haskell. O Captulo
termina com um estudo sobre os tipos abstratos de dados e sobre lazy evaluation, utilizada na
criacao de listas potencialmente innitas.

5.2

Classes de tipos

J
a foram vistas funcoes que atuam sobre valores de mais de um tipo. Por exemplo, a funcao
length pode ser empregada para determinar o tamanho de listas de qualquer tipo. Assim,
length e uma funcao polim
orca, ou seja, com apenas uma u
nica denicao, ela pode ser aplicada
a uma variedade de tipos. Por outro lado, algumas funcoes podem ser aplicadas a apenas alguns
tipos de dados, mas nao podem ser aplicadas a todos, e tem de apresentar varias denicoes, uma
para cada tipo. S
ao os casos das funcoes +, -, /, etc. Estas funcoes sao sobrecarregadas.
109

Existe uma discussao antiga entre os pesquisadores sobre as vantagens e desvantagens de se


colocar sobrecarga em uma linguagem de programacao. Alguns argumentam que a sobrecarga
n
ao aumenta o poder de expressividade da linguagem, uma vez que as denicoes sao distintas,
apesar de terem o mesmo nome. Neste caso, elas poderiam ter nomes distintos para cada
denicao. Outros admitem a sobrecarga como uma necessidade das linguagens, uma vez que ela
permite reusabilidade e legibilidade como vantagens [35]. Por exemplo, seria tedioso usar um
smbolo para a soma de inteiros e outro para a soma de n
umeros reais. Esta mesma situacao se
verica nos operadores de subtracao e multiplicacao. A verdade e que todas as linguagens de
programacao admitem alguma forma de sobrecarga. Haskell n
ao e uma excecao e admite que
seus operadores aritmeticos pre-denidos sejam sobrecarregados.
Os tipos sobre os quais uma funcao sobrecarregada pode atuar formam uma colecao de tipos
chamada classe de tipos (ou type class) em Haskell. Um tipo que pertence a uma classe e dito
ser uma inst
ancia desta classe. Entre as classes de Haskell pode existir uma forma de heranca,
como nas linguagens orientadas a objeto.

5.2.1

Fundamenta
c
ao das classes

A funcao elem, aplicada a um elemento e uma lista de valores do tipo deste mesmo elemento,
verica se este elemento pertence, ou nao, a` lista, retornando um valor booleano. Sua denicao
e a seguinte:
elem :: t -> [t] -> Bool
elem x [ ] = False
elem x (a : y) = x == a || elem x y
Analisando a denicao da funcao aplicada `a lista nao vazia, vericamos que e feito um teste
para vericar se o elemento x e igual a cabeca da lista (x==a). Para este teste e utilizada a
funcao de igualdade (==). Isto implica que a funcao elem so pode ser aplicada a tipos cujos
valores possam ser comparados pela funcao ==. Os tipos que tem esta propriedade formam
uma classe que, em Haskell, e denotada por Eq.
Em Haskell, existem dois operadores de igualdade: == e /=. Para valores booleanos, eles
sao denidos da seguinte forma:
(/=), (==) :: Bool -> Bool -> Bool
x == y = (x and y) or (not x and not y)
x /= y = not (x == y)
importante observar a diferenca entre == e =. O smbolo == e usado para denotar um
E
teste computacional para a igualdade, enquanto o smbolo = e usado nas denicoes e no sentido
matematico normal. Na Matem
atica, a assertiva double = square e uma armacao falsa e a
assertiva = e verdadeira, uma vez que qualquer coisa e igual a si pr
opria. No entanto, as
funcoes nao podem ser testadas quanto `a sua igualdade e o resultado da da avaliacao == e
tambem e n
ao True. Isto n
ao quer dizer que o avaliador seja uma m
aquina n
ao matematica
e 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 uma
variedade de tipos distintos, n
ao apenas no tipo Bool. Em outras palavras, == e /= sao
operadores sobrecarregados. Estas operacoes serao denidas de forma diferente para cada tipo
e a forma adequada de introduz-las e declarar uma classe de todos os tipos para os quais ==
e /= v
ao ser denidas. Esta classe e pre-denida em Haskell e e denominada Eq.
110

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, ou
metodos, == e /=. Estas funcoes tem o seguinte tipo:
(==), (/=) :: Eq t => t -> t -> Bool
Agora o leitor pode entender o resultado mostrado pelo sistema Haskell quando detecta
algum erro de tipo na declaracao de funcoes. Normalmente, o sistema se refere a alguma classe
de tipos nestes casos.

5.2.2

Fun
c
oes que usam igualdade

A funcao
todosIguais :: Int -> Int -> Int -> Bool
todosIguais m n p = (m == n) && (n == p)
verica se tres valores inteiros sao iguais, ou nao. No entanto, em sua denicao, n
ao e feita
qualquer restricao que a obrigue a ser denida somente para valores inteiros. A u
nica exigencia
feita 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 comparados
pela funcao ==. Isto d
a a` funcao todosIguais um tipo mais geral da seguinte forma:
todosIguais :: Eq t => t -> t -> t -> Bool
signicando que ela n
ao e utilizada apenas sobre os tipos inteiros, mas sim, a tipos que estejam
na classe Eq, ou seja, para os quais seja denida uma funcao de igualdade (==). A parte antes
do sinal => e chamada de contexto. A leitura deste novo tipo deve ser: se o tipo t esta na classe
Eq, ou seja, se a funcao de igualdade, == estiver denida para este tipo t, ent
ao todosIguais
tem o tipo t > t > t > Bool. Isto signica que a funcao todosIguais pode ter os
seguintes tipos, entre outros: Int > Int > Int > Bool ou Char > Char > Char
> Bool ou ainda (Int, Bool) > (Int, Bool) > (Int, Bool) > Bool, uma vez que
todos estes tipos tem uma funcao == denida para eles.
Vejamos agora o que acontece ao tentarmos aplicar a funcao todosIguais a argumentos do
tipo funcao, como por exemplo,
suc :: Int -> Int
suc = (+1)
da seguinte forma: todosIguais suc suc suc.
O resultado mostrado pelos compiladores Haskell ou Hugs e ERROR: Int > Int is not
an instance of class Eq, signicando que n
ao existe uma denicao da funcao == para o tipo
Int > Int (o tipo de suc).
111

5.2.3

Assinaturas e inst
ancias

J
a foi visto que a operacao de igualdade (==) e sobrecarregada, o que permite que ela seja
utilizada em uma variedade de tipos para os quais esteja denida, ou seja, para as inst
ancias
da classe Eq. Ser
a mostrado agora como as classes e instancias sao declaradas. Por exemplo, a
classe Visible que transforma cada valor em uma String e d
a a ele um tamanho:
class Visible t where
toString :: t -> String
size :: t -> Int
A denicao inclui o nome da classe (Visible) e uma assinatura, que sao as funcoes que
compoem a classe juntamente com seus tipos. Um tipo t para pertencer a` classe Visible tem
de implementar as duas funcoes da assinatura, ou seja, coisas visveis sao coisas que podem ser
transformadas em uma String e que tenham um tamanho. Para declarar o tipo Char como
uma inst
ancia da classe Visible, devemos fazer a declaracao
instance Visible Char where
toString ch = [ch]
size _ = 1
que mostra como um caractere pode ser transformado em uma String de tamanho 1. De forma
similar, para declararmos o tipo Bool como uma instancia da classe Visible, temos de declarar
instance
toString
toString
size _ =

Visible Bool where


True = "True"
False = "False"
1

Para que o tipo Bool seja uma inst


ancia da classe Eq devemos fazer:
instance Eq Bool where
True == True = True
False == False = True
_ == _ = False

5.2.4

Classes derivadas

Uma classe pode herdar as propriedades de outras classes, como nas linguagens orientadas a
objetos. Como exemplo, vamos observar a classe Ord que possui as operacoes >, >=, <, <=,
max, min e compare, alem de herdar a operacao == da classe Eq. Sua denicao e feita da
seguinte forma:
class Eq t => Ord a where
(<), (<=), (>), (>=) :: t -> t -> Bool
max, min :: t -> t -> t
compare :: t -> t -> Ordering
O tipo Ordering sera denido mais adiante, quando nos referirmos aos tipos algebricos
de dados. Neste caso, a classe Ord herda as operacoes de Eq (no caso, apenas ==). H
a a
necessidade de se denir pelo menos a funcao < (pode ser outra) para ser utilizada na denicao
das outras funcoes da assinatura. Estas denicoes formam o conjunto de declaracoes a seguir:
112

x <= y = (x < y || x == y)
x > y = y < x
x >= 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 denidas anteriormente. Para ordenar a lista e necessario
que 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 m
ultiplas tambem pode ocorrer em uma declaracao de inst
ancia 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. As
restricoes m
ultiplas tambem podem ocorrer na denicao de uma classe. Por exemplo,
class (Ord a, Visible a) => OrdVis a
signicando 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 uma
assinatura vazia. Para estar na classe OrdVis, um tipo deve semplesmente estar nas classes
Ord e Visible. Neste caso, a denicao da funcao vSort anterior poderia ser modicada para
vSort :: OrdVis t => [t] -> String
O caso em que uma classe e construda a partir de duas ou mais classes e chamado de heranca
m
ultipla.
Exerccios
1. Como voce colocaria Bool, o tipo par (a, b) e o tipo tripla (a, b, c) como inst
ancias
do tipo Visible?
2. Dena uma funcao para converter um valor inteiro em uma String e mostre como Int
pode ser uma inst
ancia de Visible.
3. Qual o tipo da funcao compare x y = size x <= size y?

5.2.5

As classes pr
e-definidas em Haskell

Haskell contem algumas classes pre-denidas e vamos ver algumas delas com alguma explanacao
sobre a sua utilizacao e denicao.
A classe Eq
Esta classe, ja descrita anteriormente, contem os tipos para os quais sao denidas funcoes
de igualdade ==. Estas denicoes usam as seguintes denicoes default:
113

class Eq t where
(==), (/=) :: t -> t -> Bool
x /= 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 denicao e:
class (Eq t) => Ord t where
compare :: t -> t -> Ordering
(<), (<=), (>=), (>) :: t -> t -> Bool
max, min :: t -> t -> t
onde o tipo Ordering tem tres resultados possveis: LT, EQ e GT, que s
ao os resultados
possveis de uma comparacao entre dois valores. A denicao de compare e:
compare x y
|x == y
|x <= y
|otherwise

= EQ
= LT
= GT

A vantagem de se usar a funcao compare e que muitas outras funcoes podem ser denidas
em funcao dela, por exemplo,
x
x
x
x

<= y
< y
>= y
> y

=
=
=
=

compare
compare
compare
compare

x
x
x
x

y
y
y
y

/=
==
/=
==

GT
LT
LT
GT

As funcoes max e min sao denidas 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 funcoes
e os tipos abstratos de dados, um tema que sera visto mais adiante, ainda neste Captulo.
A classe Enum
Esta e a classe dos tipos que podem ser enumerados, por exemplo, a lista [1,2,3,4,5,6] pode
tambem ser descrita por [1 .. 6] ou usando as funcoes da classe Enum, cuja denicao e a
seguinte:
class (Ord t) => Enum a where
toEnum :: Int -> t
fromEnum :: t -> Int
enumFrom :: t -> [t]
enumFromThen :: t -> t -> [t]
enumFromTo :: t -> t -> [t]
enumFromThenTo :: t -> t -> t -> [t]
114

-----

[n ..]
[n, m ..]
[n .. m]
[n, n .. m]

As funcoes fromEnum e toEnum tem as funcoes ord e chr do tipo Char como correspondentes, ou seja ord e chr sao denidas usando fromEnum e toEnum. Simon Thompson [35]
arma que o Haskell report estabelece que as funcoes toEnum e fromEnum n
ao sao signicativas para todas as inst
ancias da classe Enum. Para ele, o uso destas funcoes sobre valores de
ponto utuante ou inteiros de precis
ao completa (Integer) resulta em erro de execucao.
A classe Bounded
Esta e uma classe que apresenta um valor mnimo e um valor m
aximo para a classe. Suas
instancias sao Int, Char, Bool e Ordering. Sua denicao e a seguinte:
class Bounded t where
minBound, maxBound :: t
que retornam os valores mnimos e maximos de cada tipo.
A classe Show
Esta classe contem os tipos cujos valores podem ser escritos como String. A maioria dos
tipos pertencem a esta classe. Sua denicao e a seguinte:
type ShowS = String -> String
class Show a where
showsPrec :: Int -> a -> ShowS
show :: a -> String
showList :: [a] -> ShowS
A classe Read
Esta classe contem os tipos cujos valores podem ser lidos a partir de strings. Para usar a
classe e necessario apenas conhecer a funcao read :: Read t => String > t. Esta classe
complementa a classe Show uma vez que as strings produzidas por show sao normalmente
possveis de serem lidas por read.
read :: Read t => String -> t

5.3

Tipos alg
ebricos

J
a foram vistas v
arias formas de modelar dados em Haskell. Vimos as funcoes de alta ordem,
polimorsmo e as classes de tipos (type class) como metodologia para implementar sobrecarga.
Estes dados foram modelados atraves dos seguintes tipos:
Tipos b
asicos (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 s
o, j
a mostram o grande poder de expressividade e de abstracao que elas proporcionam. No entanto, outros tipos de dados precisam ser
modelados. Por exemplo,
os meses: janeiro, ..., dezembro;
tipos cujos elementos sejam n
umeros ou strings. Por exemplo, uma casa em uma rua
pode ser identicada por um n
umero ou pelo nome da famlia. (n
ao no Brasil);
o tipo arvore.
115

Para construir um novo tipo de dados, usamos a declaracao data que descreve como os elementos deste novo tipo de dados s
ao construdos. Cada elemento e nomeado por uma expressao
formulada em funcao dos construtores do tipo. Alem disso, nomes diferentes denotam elementos tambem distintos. Atraves do uso de pattern matching sobre os construtores, projetam-se
operacoes que geram e processam elementos do tipo de dados escolhidos pelo programador. Os
tipos assim descritos, nao as operacoes, sao chamados tipos concretos [35, 4] e sao modelados
em Haskell atraves dos tipos algebricos.

5.3.1

Como se define um tipo alg


ebrico?

Um tipo algebrico e mais uma facilidade que Haskell oferece, proporcionando ao programador
mais poder de abstracao, necessario para modelar algumas estruturas de dados complexas. Outras possibilidades tambem existem, como por exemplo, os tipos abstratos de dados a serem
vistos 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 uni
ao disjunta de conjuntos. Como exemplo destes tipos podemos citar:
data Tempo = Frio | Quente
data Estacao = Primavera | Verao | Outono | Inverno
Neste caso, Frio e Quente sao os construtores do tipo Tempo e Primavera, Ver
ao,
Outono e Inverno sao os construtores do tipo Estacao.
As funcoes sobre estes tipos sao declaradas usando pattern matching. Para descrever a
temperatura das estacoes podemos usar:
temperatura :: Estacao -> Tempo
temperatura Verao = Quente
temperatura _ = Frio
Produtos de tipos
Um produto de tipos e um novo tipo de dados, cujos valores s
ao construdos com mais de
um construtor. Por exemplo,
data Gente = Pessoa Nome Idade
type Nome = String
type Idade = Int
A leitura de um valor do tipo Gente deve ser feita da seguinte forma: para construir um
elemento 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 funciona
como uma funcao aplicada aos outros dois objetos, n e i. Exemplos deste tipo podem ser:
Pessoa "Constantino" 5
Pessoa "Dagoberto" 2
116

Podemos formar uma funcao mostraPessoa que toma um elemento do tipo Gente e o mostra
na tela:
mostraPessoa :: Gente -> String
mostraPessoa (Pessoa n a) = n ++ " -- " ++ show a
Neste caso, a aplicacao da funcao mostraPessoa (Pessoa John Lennon, 60) sera
respondida por
>"John Lennon -- 60"
Neste caso, o tipo tem um u
nico construtor, Pessoa, que tem dois elementos, Nome e
Idade, para formar o tipo Gente. Nos tipos enumerados, Tempo e Estacao, os construtores
sao nul
arios (0-arios) porque n
ao tem argumentos. Assim, Pessoa n a pode ser interpretado
como sendo o resultado da aplicacao da funcao Pessoa aos argumentos n e a, ou seja, Pessoa
:: Nome > Idade > Gente.
Uma outra denicao de tipo para Gente poderia ser
type Gente = (Nome, Idade)
Existem vantagens e desvantagens nesta nova vers
ao, no entanto, e senso comum que ela deve ser
evitada. Podemos usar o mesmo nome para o tipo e para o construtor, no entanto, esta escolha
pode conduzir a ambig
uidades e, portanto, deve ser evitada. Por exemplo, e perfeitamente legal
a construcao do tipo data Pessoa = Pessoa Nome Idade.
Exemplo. Uma forma geometrica pode ser um crculo ou um ret
angulo. Ent
ao podemos
modela-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 que
calcula a area de uma forma geometrica:
area :: Forma -> Float
area (Circulo r) = pi * r * r
area (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 n


umero inteiro n
ao negativo
(pode ser zero).
Os tipos podem ser recursivos, ou seja, o tipo NomedoTipo pode ser usado como parte
a listas, arvores e muitas outras estruturas de dados. Estes tipos serao
dos tipos tij . Isto nos d
vistos mais adiante neste Captulo.
O NomedoTipo pode ser seguido de uma ou mais vari
aveis de tipo que podem ser usadas
no lado direito da denicao, tornando-a polim
orca.
117

5.3.3

Derivando inst
ancias de classes

Ao se introduzir um tipo algebrico, como Estacao, podemos desejar que ele tambem tenha
igualdade, enumeracao, etc. Isto pode ser feito pelo sistema, informando que o tipo tem as
mesmas funcoes que as classes Eq, Ord e Show tem:
data

Estacao = Primavera | Verao | Outono | Inverno


deriving (Eq, Ord, Show)

Exerccios:
1. Redena a funcao temperatura :: Estacao > Tempo de forma a usar guardas em
vez de pattern matching. Qual denicao deve ser preferida, em sua opini
ao?
2. Dena o tipo Meses como um tipo algebrico em Haskell. Faca uma funcao que associe
um mes `a sua estacao. Coloque ordenacao sobre o tipo.
3. Dena uma funcao que de o tamanho do permetro de uma forma geometrica do tipo
Forma.
4. Adicione um construtor extra ao tipo Forma para tri
angulos e estenda as funcoes area e
perimetro (exerccio anterior) para incluir os tri
angulos.
5. Dena uma funcao que decida quando uma forma e regular. Um crculo e regular, um
quadrado e um retangulo regular e um tri
angulo equilatero e regular.

5.3.4

Tipos recursivos

Exemplo. Uma express


ao aritmetica simples que envolve apenas adicoes e subtracoes pode ser
modelada atraves de sua BNF. Usando um tipo algebrico podemos modelar uma expressao da
seguinte 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 2
2 + 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 como
resultado o valor da expressao. Assim podemos fazer
eval
eval
eval
eval

:: Expr
(Lit n)
(Add e1
(Sub e1

-> Int
= n
e2) = (eval e1) + (eval e2)
e2) = (eval e1) - (eval e2)

Esta denicao e primitiva recursiva, ou seja, existe uma denicao para um caso base (Lit n)
e uma denicao recursiva para os casos indutivos. Por exemplo, uma funcao que imprime uma
expressao pode ser feita da seguinte maneira:
mostraExpressao :: Expr -> String
mostraExpressao (Lit n) = show n
118

mostraExpressao (Add e1 e2)


= "(" ++ mostraExpressao e1 ++ "+" ++ mostraExpressao e2 ++ ")"
mostraExpressao (Sub e1 e2)
= "(" ++ mostraExpressao e1 ++ "-" ++ mostraExpressao e2 ++ ")"
Exerccio. Construir uma funcao que calcule o n
umero de operadores em uma expressao.

Arvores bin
arias de inteiros
Uma arvore bin
aria ide inteiros ou e nula ou composta de n
os internos e externos. Cada n
o
ou e a arvore nula ou e um n
umero inteiro com duas sub-
arvores bin
arias de inteiros como seus
descendentes. Para os n
os internos, as duas sub-
arvores n
ao podem ser ambas nulas e para as
folhas elas sao nulas. Uma maneira de se modelar estas arvores em Haskell e simplesmente
data ArvoreInt = Nil | No Int ArvoreInt ArvoreInt
As arvores bin
arias de inteiros podem ser mostradas gracamente, conforme a Figura 5.1.

Figura 5.1: Representacoes de arvores bin


arias de inteiros.
Agora podemos construir funcoes para manipular estas arvores. Por exemplo, uma funcao
para somar os elementos da arvore:
somaArvInt :: ArvoreInt -> Int
somaArvInt Nil = 0
somaArvInt (No n t1 t2) = n + somaArvInt t1 + somaArvInt t2
Uma funcao que retorne a profundidadede uma arvore bin
aria de inteiros, levando em consideracao que as profundidades de uma arvore Nil e da raiz de uma arvore No sao 0 (zero):
profundidade :: ArvInt -> Int
profundidade Nil = 0
profundidade (No n t1 t2)
|(t1 == Nil) && (t2 == Nil) = 0
|otherwise
= 1 + max (profundidade t1) (profundidade t2)
Ou podemos modelar uma funcao que verique quantas vezes um n
umero p aparece em uma
arvore do tipo ArvoreInt:
numOcorrencias :: ArvoreInt -> Int -> Int
119

numOcorrencias
numOcorrencias
|n == p
=
|otherwise =

5.3.5

Nil p = 0
(No n t1 t2) p
1 + (numOcorrencias t1 p) + (numOcorrencias t2 p)
(numOcorrencias t1 p) + (numOcorrencias t2 p)

Recurs
ao m
utua

Uma propriedade muito difcil de ser implementada em qualquer linguagem de programacao, seja
imperativa ou pertencente a outro paradigma, e recursao m
utua. Por este motivo, a maioria das
linguagens n
ao permite esta facilidade. No entanto, Haskell a oferece, dando um maior poder de
abstracao ao programador. Observamos, no entanto, que este tipo de recurs
ao deve ser utilizado
com 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)
Exerccios:
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-dena as
funcoes eval, showExpr e size (que d
a a quantidade de operadores em uma expressao).
O que sua operacao de divis
ao faz no caso da divis
ao ser por zero?
3. Calcule: somaArvInt (No 3 (No 4 Nil Nil) Nil) e
(No 4 Nil Nil) Nil), passo a passo.

profundidade (No 3

4. Dena uma funcao que decida se um n


umero inteiro e um elemento de uma ArvoreInt.
5. Dena funcoes para encontrar os valores maximo e mnimo mantidos em uma arvore de
inteiros do tipo ArvoreInt.
6. Dena as funcoes transfArvLista, sort :: ArvoreInt > [Int] que transformam uma
arvore de inteiros em uma lista de inteiros. A funcao transfArvLista deve percorrer a
sub-
arvore da esquerda, depois a raiz e, nalmente, a sub-
arvore da direita (in-order). A
funcao sort deve ordenar os elementos da arvore de tal forma que os elementos da lista
resultante 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

possvel estender o tipo Expr para que ele contenha express


7. E
oes condicionais IF b e1
e2, 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 for
False.
dataBExp = BoolLit Bool
|And BExp BExp
|Not BExp
|Equal Expr Expr
|Greater Expr Expr
Estas cinco claulas d
ao os seguintes valores:
Literais booleanos: BoolLit True e BoolLit False.
A conjuncao de duas express
oes: e True se as duas sub-expressoes argumentos tiverem o valor True, caso contrario, o resultado ser
a False.
A negacao de uma expressao: Not be tem o valor True se be for False.
A igualdade de duas express
oes: Equal e1 e2 e True se as duas expressoes numericas
tiverem valores iguais.
A ordem maior: Greater e1 e2 e True se a expressao numerica e1 tiver um valor
maior que a expressao numerica e2.
A partir destes pressupostos, dena as funcoes:
eval :: Expr -> Int
bEval :: BExp -> Bool
por recurs
ao m
utua e estenda a funcao show para mostrar o tipo re-denido para expressoes.

5.3.6

Tipos alg
ebricos polim
orficos

As denicoes de tipos algebricos podem conter tipos vari


aveis como t, u, etc. Por exemplo,
data Pares t = Par t t
e podemos ter
Par 2 3 :: Pares Int
Par [ ] [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 -> Bool
igualPar (Par x y) = (x == y)
121

As listas, j
a vistas anteriormente, podem ser construdas a partir de tipos algebricos.
data List t = Nil | Cons t (List t)
deriving (Eq, Ord, Show)

Arvores
bin
arias em geral
As arvores bin
arias vistas anteriormente sao arvores bin
arias de inteiros. Mas podemos
construir arvores bin
arias onde seus nos sejam de um tipo vari
avel.
data Arvore t = Nil | No t (Arvore t) (Arvore t)
deriving (Eq, Ord, Show)
As funcoes que manipulam arvores bin
arias de inteiros devem ser re-denidas para tratar
com esta nova estrutura, apesar das diferencas serem mnimas. Com relacao a` profundidade
deve-se observar que esta funcao se refere a cada no e n
ao a uma arvore como um todo. Dito de
outra forma, cada n
o de uma arvore tem uma profundidade diferente, a n
ao ser que eles estejam
no mesmo nvel. Assim, para encontrar a profundidade de cada n
o em uma arvore de algum tipo
t, e necessario fazer uma transformacao desta arvore em uma arvore de tuplas onde o primeiro
elemento e um valor do tipo t e o segundo e a profundidade do n
o. Se a arvore em questao
for constituda de inteiros, Arvore Int, uma outra possibilidade e transformar esta arvore de
inteiros em uma arvore de lista de inteiros. Para o caso de transformacao em uma arvore de
tuplas, a denicao pode ser feita da seguinte forma:
transforma :: Arvore t -> Int -> Arvore (t,Int)
transforma Nil n
= Nil
transforma (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 construda facilmente da
seguinte 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 transformandoa em outra arvore, fazendo o papel de mapeamento, pode ser denida como segue:
mapArvore :: (t -> u) -> Arvore t -> Arvore u
mapArvore f Nil = Nil
mapArvore f (No x t1 t2) = No (f x ) (mapTree f t1) (mapArvore f t2)
O tipo uni
ao
As denicoes tambem podem tomar mais de um parametro e podemos formar um tipo cujos
elementos sejam de um tipo t ou de um tipo u.
data Uniao t u = Nome t | Numero u
122

Membros desta uni


ao sao Nome a, com a :: t, ou Numero b, com b :: u. Um tipo que seja
um nome ou um n
umero, pode ser dado por
Nome "Richard Nixon" :: Uniao String Int
Numero 2143 :: Uniao Strig Int
Podemos agora formar uma funcao que verique quando um elemento est
a na primeira parte
da uni
ao ou n
ao:
eNome :: Uniao t u -> Bool
eNome (Nome _)
= True
eNome (Numero _) = False
Para denir uma funcao de Uniao t u para Int (por exemplo), devemos tratar com os dois
casos:
fun :: Uniao t u -> Int
fun (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 -> v
juntaFuncoes f g (Nome x)
= f x
juntaFuncoes f g (Numero y) = g y
Se tivermos uma funcao f :: t > v e desejarmos aplica-la a um elemento do tipo Uniao
t u, vai ocorrer um problema que e o de n
ao sabermos se o elemento pertence `a primeira ou a`
segunda parte da uni
ao. Se a denicao dada para a primeira parte for aplicada a` segunda parte
ocorrer
a um erro.
aplicaUm :: (t -> v) -> Uniao t u -> v
aplicaUm f (Nome x)
= f x
aplicaUm f (Numero _) = error "aplicaUm applied to Dois"
Exerccios:
1. Dena uma funcao twist que troca a ordem de uma uni
ao. twist :: Uniao t u > Uniao
u t. Qual ser
a o efeito da aplicacao (twist . twist)?
2. As arvores denidas anteriormente s
ao bin
arias, ou seja, cada n
o tem exatamente duas
sub-
arvores. Podemos, no entanto, denir arvores mais gerais com uma lista arbitraria de
sub-
arvores. Por exemplo,
data ArvoreGeral t = Folha t | No [ArvoreGeral t]
dena funcoes para:
(a) contar o n
umero de folhas em uma ArvoreGeral,
(b) encontrar a profundidade de uma ArvoreGeral,
(c) somar os elementos de uma arvore generica,
(d) vericar 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

5.4

Tratamento de erros

Para se construir bons programas, e necessario que especicar o que o programa deve fazer
no caso de acontecer algumas situacoes anomalas. Estas situacoes anomalas sao chamadas de
excecoes e, entre elas, podemos citar:
tentativa de divis
ao por zero, calculo de raiz quadrada de n
umero negativo ou a aplicacao
da funcao fatorial a um n
umero 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 e
exibir uma mensagem sobre o motivo da ocorrencia e parar a execucao. Isto e feito atraves de
uma funcao de erro.
error :: String -> t
Uma tentativa de avaliar a express
ao error Circulo com raio negativo.resultaria na mensagem
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, sem
parar a execucao do programa. Isto pode ser feito atraves das duas tecnicas a seguir.

5.4.1

Valores fictcios

A funcao tail e construda para retornar a cauda de uma lista nita n


ao vazia e, se a lista for
vazia, reportar esta situacao com uma mensagem de erro e parar a execucao. Ou seja,
tail :: [t] -> [t]
tail (a : x) = x
tail [ ] = error "cauda de lista vazia"
No entanto, esta denicao poderia ser refeita da seguinte forma:
tl :: [a] -> [a]
tl (_:xs) = xs
tl [ ]
= [ ]
Desta forma, todas as listas teriam uma resposta para uma solicitacao de sua cauda, seja ela
vazia ou n
ao. De forma similar, a funcao de divis
ao de dois n
umeros inteiros poderia ser feita
da seguinte forma, envolvendo o caso onde o denominador seja zero:
divide :: Int -> Int -> Int
divide n m
|(m /= 0) = n div m
|otherwise = 0
124

Para estes dois casos, a escolha dos valores ctcios e obvia. No entanto, existem casos em
que esta escolha nao e possvel. Por exemplo, na denicao da cabeca de uma lista. Neste caso,
qual seria um valor ctcio adequado? Para resolver esta situacao, temos de recorrer a um
artifcio mais elaborado. Precesamos re-denir a funcao hd, acrescentando mais um par
ametro.
hd :: a -> [a] -> a
hd y (x:_) = x
hd y [ ]
= y
Esta tecnica e mais geral e consiste na denicao de uma nova funcao para o caso de ocorrer
erro. Neste caso, a funcao de um argumento foi modicada para ser aplicada a dois argumentos.
De uma forma geral, temos de construir uma funcao com uma denicao para o caso da excecao
acontecer e outra denicao para o caso em que ela nao aconteca.
fErr y x
|cond
= y
|otherwise = f x
Esta tecnica funciona bem em muitos casos. O u
nico problema e que n
ao e reportada
nenhuma mensagem informando a ocorrencia incomum. Uma outra abordagem e processar a
entrada indesej
avel.

5.4.2

Tipos de erros

As tecnicas anteriores se baseiam no retorno de valores ctcios na ocorrencia de um erro. No


entanto, 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 t
deriving (Eq, Ord, Read, Show)
Na realidade e o tipo t com um valor extra, Nothing, acrescentado. Assim podemos denir
a funcao de divis
ao errDiv da seguinte forma:
errDiv :: Int -> Int -> Maybe Int
errDiv 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 sada original, digamos t, mas do tipo
Maybe t. Este tipo, Maybe, nos permite processar um erro. Podemos fazer duas coisas com
ele:
podemos transmitiro erro atraves de uma funcao, que e o efeito da funcao mapMaybe,
a ser vista a seguir e
125

podemos segurarum erro, que e o papel da funcao maybe.


A funcao mapMaybe transmite o valor de um erro, apesar da aplicacao de uma funcao
g. Suponhamos que g seja uma funcao do tipo a b e que estejamos tentando usa-la como
operador sobre um tipo Maybe a. No caso do argumento Just x, g pode ser aplicada a x para
dar o rsultado g x do tipo b. Por outro lado, se o argumento for Nothing ent
ao Nothing e o
resultado.
mapMaybe :: (a -> b) -> Maybe a -> Maybe b
mapMaybe g Nothing = Nothing
mapMaybe g (Just x) = Just (g x)
Para amarrar um erro, deve-se retornar um resultado do tipo b, a partir de uma entrada do
tipo 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, sao
usados nos casos Nothing e Just, respectivamente.
maybe :: b -> (a -> b) -> Maybe a -> b
maybe n f Nothing = n
maybe n f (Just x) = f x
Podemos ver as funcoes mapMaybe e maybe em acao, nos exemplos que seguem. No
primeiro deles, a divis
ao por zero nos retorna Nothing que vai sendo empurradopara a frente
e 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 por
3 e maybe, no nvel 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 denir o sistema sem gerenciar os erros e depois
adicionar um gerenciamento de erro usando as funcoes mapMaybe e maybe juntamente com
as funcoes modicadas para segurar o erro. Separar o problema em duas partes facilita a solucao
de cada uma delas e do todo.
Exerccio.
Dena uma funcao process :: [Int] > Int > Int > Int de forma que process l n
m toma o n-esimo e o m-esimo elementos de uma lista l e retorna a sua soma. A funcao deve
retornar zero se quaisquer dos n
umeros n ou m n
ao forem ndices da lista l. Para uma lista de
tamanho p, os ndices sao: 0, 1, ..., p-1, inclusive.
126

5.5

Provas sobre tipos alg


ebricos

Com os tipos algebricos tambem podemos realizar provas sobre a corretude ou nao de alguns
programas, de forma similar `as provas com outros tipos de dados. Por exemplo, reformulando a
denicao de
arvore bin
aria, 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 nitas do tipo Arvore t, temos
de analisar dois casos:
1. o caso Nil: vericar se P(Nil) e verdadeira e
2. o caso No: vericar 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 denicoes anteriores:
map f [ ] = [ ]
map (a : x) = f a : map f x

(1)
(2)

mapTree f Nil = Nil


mapTree f (No x t1 t2 )
= No (f x) (mapTree f t1 ) (mapTree f t2 )

(3)

collapse Nil = [ ]
collapse (No x t1 t2 )
= collapse t1 ++ [x] ++ collapse t2

(5)

(4)

(6)

Esquema da prova:
O caso Nil:
Lado esquerdo
map f (collapse Nil)
= map f [ ]
=[]

por (5)
por (1)

Lado direito
collapse (mapTree f Nil)
= collapse Nil
=[]

por (3)
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) e
map 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.
Conclus
ao: a propriedade e valida para os casos nil e No. Portanto, e valida para qualquer
arvore bin
aria, ou seja,
127

Lado esquerdo
map f (collapse(No x tr1 tr2))
=map f (collapse tr1 ++ [x] ++
collapse tr2)
=map f (collapse tr1) ++ [f x] ++
map f (collapse tr2)
=collapse (mapTree f tr1) ++ [f x]
++ collapse(mapTree f tr2)

por (6)
por (9)

Lado direito
collapse (mapTree f (No x tr1 tr2))
= collapse (No (f x)
(mapTree f tr1) (mapTree f tr2))

por (4)

=collapse (mapTree f tr1) ++ [f x]


++ collapse(mapTree f tr2)

por (6)

por (7 e 8)

map f (collapse tr) = collapse (mapTree f tr)


Exerccios:
1. Usando a denicao de profundidade de uma arvore bin
aria, feita anteriormente, prove
que, para toda arvore nita de inteiros tr, vale profundidade tr < 2(prof undidade tr) .
2. Mostre que para toda arvore nita de inteiros tr,
occurs tr a = length (filter (==a) (collapse tr)).
3. Prove que a funcao twist, denida anteriormente, tem a propriedade de que twist . twist
= id.

5.6

M
odulos em Haskell

No Captulo introdut
orio desta Apostila, armamos que a principal vantagem das linguagens
estruturadas era a modularidade oferecida por elas e, por este motivo, as linguagens funcionais
eram indicadas como solucao para a crise do software dos anos 80. De fato, as linguagens
funcionais s
ao modulares e Haskell oferece muitas alternativas para a construcao de modulos,
obedecendo `as exigencias preconizadas pela Engenharia de Software para a construcao de programas.
Para John Hughes [15], a modularidade e a caracterstica que uma linguagem de programacao
deve apresentar para que seja utilizada com sucesso. Ele arma que esta e a caracterstica principal das linguagens funcionais, proporcionada atraves das funcoes de alta ordem e do mecanismo
de avaliacao lazy.
A modularidade e importante porque:
as partes de um sistema podem ser construdas 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 benefcios sejam auferidos,
caso contrario, em vez de facilitar pode complicar a sua utilizacao. Entre os cuidados que devem
ser levados em consideracao podem ser citados:
cada modulo deve ter um papel claramente denido,
cada modulo deve fazer exatamente uma u
nica tarefa,
cada modulo deve ser auto-contido,
cada modulo deve exportar apenas o que e estritamente necessario e
os modulos devem ser pequenos.
128

5.6.1

Cabe
calho em Haskell

Cada modulo em Haskell constitui um arquivo. A forma de se declarar um m


odulo e a seguinte:
module Formiga where
data Formigas = . . .
comeFormiga x = . . .
Deve-se ter o cuidado de que o nome do arquivo deve ter a extens
ao .hs ou .lhs e deve ter o
mesmo nome do modulo. Neste caso, o arquivo deve ser Formiga.hs ou Formiga.lhs.

5.6.2

Importa
c
ao de m
odulos

Os modulos em Haskell podem importar dados e funcoes de outros modulos da seguinte forma:
module Abelha where
import Formiga
pegadordeAbelha = . . .
As denicoes visveis em Formiga podem ser utilizadas em Abelha.
module Vaca where
import Abelha
Neste caso, as denicoes Formiga e comeFormiga n
ao sao visveis em Vaca. Elas podem
se tornar visveis pela importacao explcita de Formiga ou usando os controles de exportacao
para modicar o que e exportado a partir de Abelha.

5.6.3

O m
odulo main

Em todo sistema deve existir um m


odulo chamado Main que deve conter a denicao da funcao
main. Em um sistema interpretado, como Hugs, ele tem pouca import
ancia porque um m
odulo
sem um nome explcito e tratado como Main.

5.6.4

Controles de exportac
ao

Por default, tudo o que e declarado em um modulo pode ser exportado, e apenas isto, ou seja, o
que e importado de outro m
odulo n
ao pode ser exportado pelo m
odulo importador. Esta regra
pode ser exageradamente permissiva porque pode ser que algumas funcoes auxiliares n
ao devam
ser exportadas e, por outro lado, pode ser muito restritiva porque pode existir alguma situacao
em que seja necessario exportar algumas denicoes declaradas em outros modulos.
Desta forma, deve-se poder controlar o que deve ou n
ao ser exportado. Em Haskell, isto e
declarado da seguinte forma: module Abelha (pegadorAbelha, Formigas(. . .), comeFormiga) where . . . ou equivalentemente module Abelha (module Abelha, module
Formiga) where . . .
A palavra module dentro dos parenteses signica que tudo dentro do m
odulo e exportado,
ou seja, module Abelha where e equivalente a module Abelha (module Abelha) where.
129

5.6.5

Controles de importa
c
ao

Da mesma forma que Haskell tem controles para exportacao, tem tambem controles para a
importacao.
module Tall where
import Formiga (Formigas (. . .))
Neste caso, a intencao e importar apenas o tipo Formigas do m
odulo Formiga. Pode-se
tambem esconder alguma entidade. Por exemplo,
module Tall where
import Formiga hiding (comeFormiga)
Nesta situacao, a intencao e esconder a funcao comeFormiga. Se em um modulo existir
um objeto com o mesmo nome de outro objeto, denido em um m
odulo importado, pode-se
acessar ambos objetos usando um nome qualicado. Como exemplo, Formiga.urso e o objeto
importado e urso e o objeto denido localmente. Um nome qualicado e construdo a partir
do nome de um m
odulo e do nome do objeto neste m
odulo. Para usar um nome qualicado, e
necessario que se faca uma importacao:
import qualified Formiga
No caso dos nomes qualicados, pode-se tambem estabelecer quais tens vao ser exportados
e quais serao escondidos. Tambem e possvel usar um nome local para um m
odulo importado,
como em
import Inseto as Formiga
Nesta secao zemos um estudo rapido sobre a utilizacao de modulos em Haskell. O objetivo
foi apenas mostrar as muitas possibiolidades que a linguagem oferece. A pr
atica da programacao
de modulos e que vai dotar o programador da experiencia e habilidade necessarias para o desenvolvimento de programas. A pr
oxima secao e dedicada aos tipos abstratos de dados, cujo
domnio, prescinde da pr
atica da programacao e deve ser o metodo de estudo a ser adotado pelo
leitor.

5.7

Tipos abstratos de dados

O objetivo desta secao e introduzir os tipos abstratos de dados (TAD) e o mecanismo provido por
Haskell para den-los. De maneira geral, os tipos abstratos de dados diferem dos introduzidos
por uma declaracao data, no sentido de que os TADs podem escolher a representacao de seus
valores. Cada escolha de representacao conduz a uma implementacao diferente do tipo de dado
[35].
Os tipos abstratos sao denidos de forma diferente da forma utilizada para denir os tipos
algebricos. Um tipo abstrato n
ao e denido pela nomeacao de seus valores, mas pela nomeacao de
suas operacoes. Isto signica que a representacao dos valores dos tipos abstratos n
ao e conhecida.
O que e realmente de conhecimento p
ublico e o conjunto de funcoes para minipular o tipo. Por
exemplo, Float e um tipo abstrato em Haskell. Para ele, s
ao exibidas operacoes de comparacao
e aritmeticas alem de uma forma de exibicao de seus valores, mas nao se estabelece como tais
n
umeros sao representados pelo sistema de avaliacao de Haskell, proibindo o uso de casamento
130

de padr
oes. Em geral, o programador que usa um tipo abstrato n
ao sabe como seus elementos
sao representados. Tais barreiras de abstracao sao usuais quando mais de um programador
estiverem trabalhando em um mesmo projeto ou mesmo quando um mesmo programador estiver
trabalhando em um projeto n
ao trivial. Isto permite que a representacao seja trocada sem afetar
a validade dos scripts que usam o tipo abstrato. Vamos mostrar estes fundamentos atraves de
exemplos.

5.7.1

O tipo abstrato Pilha

Vamos dar incio ao nosso estudo dos tipos abstratos de dados atraves da implementacao do
tipo Pilha. As pilhas s
ao estruturas de dados homogeneas onde os valores sao colocados e/ou
retirados utilizando uma estrategia LIFO (Last In First Out). Informalmente, esta estrutura
de dados e comparada a uma pilha de pratos na qual s
o se retira um prato por vez e sempre o
que est
a no topo da pilha. Tambem so se coloca um prato por vez e em cima do prato que se
encontra no topo.
As operacoes necessarias para o funcionamento de uma pilha do tipo Stack t1 sao as seguintes:
push
pop
top
stackEmpty
newStack

::
::
::
::
::

t -> Stack
Stack t ->
Stack t ->
Stack t ->
Stack t

t -> Stack t --coloca um item no topo


Stack t
--retira um item do topo
t
--pega-se o item do topo
Bool
--verifica se a pilha eh
--cria uma pilha vazia

da pilha
da pilha
da pilha
vazia

Primeira implementa
c
ao para o TAD Pilha
Para se implementar um tipo abstrato de dados em Haskell, deve-se criar um m
odulo para isto.
A criacao de modulos foi um estudo feito na secao anterior. O leitor deve voltar a este tema se
tiver alguma diculdade de entender as declaracoes aqui feitas. Serao feitas duas implementacoes
do 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
pop
top
stackEmpty
newStack

::
::
::
::
::

t -> Stack
Stack t ->
Stack t ->
Stack t ->
Stack t

t -> Stack t
Stack t
t
Bool

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

1
Apesar do autor dar preferencia a nomes em Portugues, nestes Exemplos, eles ser
ao descritos com os nomes
com os quais foram implementados, uma vez que muitas implementaco
es j
a est
ao incorporadas ao sistema com
estes nomes e a adoca
o de outros nomes pode provocar confus
ao.

131

top (Stk x _) = x
newStack = EmptyStk
stackEmpty EmptyStk = True
stackEmpty _
= False
instance (Show t) => Show (Stack t) where
show (emptyStk) = #
show (Stk x s) = (show x) ++ | ++ (show s)
A utilizacao do tipo abstrato Stack deve ser feita em outros modulos cujas funcoes necessitam
deste tipo de dado. Desta forma, o m
odulo Stack deve constar da relacao de modulos importados
por este modulo usu
ario. Por exemplo,
module Main where
import Stack
listTOstack :: [t]
-> Stack t
listTOstack [ ]
= new Stack
listTOstack (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. A
implementacao mostrada foi baseada no tipo algebrico Stack t. O implementador pode encontrar uma forma alternativa de implementacao que seja mais eciente que esta e promover esta
mudanca sem que os usuarios saibam disto. O que n
ao pode ser modicada e a Interface com
o usu
ario, para que os programas que j
a utilizam o tipo com a implementacao antiga possam
continuar funcionando com a nova implementacao, sem qualquer modicacao no programa.
Segunda implementa
c
ao para o TAD pilha
Vamos agora mostrar uma segunda implementacao do tipo abstrato pilha baseada em listas. As
operacoes serao as mesmas, para facilitar o entendimento e a comparacao com a implementacao
anterior.
module Stack(Stack, push, pop, top, stackEmpty, newStack} where
push
pop
top
stackEmpty
newStack

::
::
::
::
::

t -> Stack
Stack t ->
Stack t ->
Stack t ->
Stack t

t -> Stack t
Stack t
t
Bool

132

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 [ ]) = True
stackEmpty _
= False
instance (Show t) => Show (Stack t) where
show (Stk [ ]) = #
show (Stk (x : xs)) = (show x) ++ | ++ (show (Stk xs))
O modulo Main e o mesmo para as duas implementacoes, nao sendo necessario ser mostrado
novamente.

5.7.2

O tipo abstrato de dado Fila

Vamos agora mostrar um exemplo do tipo abstrato la. A ideia b


asica de uma la e que ela e
uma estrutura de dados do tipo FIFO (First-In First-Out), onde os elementos sao inseridos
de um lado e s
ao retirados pelo outro (se tiver algum), imitando uma la de espera. Pode-se
dizer que uma la e uma especie de lista nita com um conjunto restrito de operacoes. Para
o programador e importante uma implementacao eciente deste tipo Queue t, porem ele nao
esta interessado como ela e feita, podendo ate solicitar a outra pessoa que o faca ou utilizar uma
solucao provida pelo sistema.
Inicialmente, o implementador precisa conhecer que operacoes primitivas sao necessarias para
o tipo abstrato a ser implementado, no caso, o tipo Queue t. Suponhamos que sejam:
enqueue
dequeue
front
queueEmpty
newQueue

::
::
::
::
::

t -> Queue
Queue t ->
Queue t ->
Queue t ->
Queue t

t -> Queue t --coloca um item no fim da fila


Queue t
--remove o item do inicio da fila
t
--pega o item da frente da fila
Bool
--testa se a fila esta vazia
--cria uma nova fila vazia

A lista de operacoes a serem providas pelo TAD, juntamente com seus tipos, constituem a assinatura do TAD, no caso, Queue t. Vamos continuar o exemplo provendo duas implementacoes
do tipo abstrato.
Primeira implementa
c
ao para o TAD Queue
A primeira implementacao a ser mostrada e baseada nas listas nitas. As implementacoes das
operacoes sao as seguintes:
module Queue (Queue, enqueue, dequeue, front, queueEmpty, newQueue) where
enqueue
:: t -> Queue t -> Queue t
133

dequeue
front
queueEmpty
newQueue

::
::
::
::

Queue
Queue
Queue
Queue

t -> Queue t
t -> t
t -> Bool
t

data Queue t = Fila [t]


enqueue x (Fila q) = Fila (q ++ [x])
dequeue (Fila (x : xs)) = Fila xs
dequeue _
= error Fila de espera vazia
front (Fila (x : _)) = x
front _
= error Fila de espera vazia
queueEmpty (Fila [ ]) = True
queueEmpty _
= False
newQueue = (Fila [ ])
instance (Show t) => Show (Queue t) where
show (Fila [ ])
= .
show (Fila (x : xs)) = < ++ (show x) ++ (show (Fila xs))
Para utilizar o TAD Queue e necessario construir o m
odulo Main, de forma similar a que
foi feita para a utilizacao do TAD Stack.
module Main where
import Stack
import Queue
queueTOstack :: Queue t -> Stack t
queueTOstack q = qts q newStack
where qts q s
|queueEmpty q = s
|otherwise
= qts (dequeue q) (push (front q) s)
stackTOqueue :: Stack t -> Queue t
stackTOqueue s = stq s newQueue
where stq s q
|stackEmpty s = q
|otherwise
= stq (pop s) (enqueue (top s) q)
invQueue :: Queue t -> Queue t
invQueue q = stackTOqueue (queueTOstack q)
invStack :: Stack t -> Stack t
invStack s = queueTOstack (stackTOqueue s)
q1 = enqueue 14 (enqueue 9 (enqueue 19 newQueue))
s1 = push 14 (push 9 (push 19 newStack))
134

Segunda implementa
c
ao do TAD Queue
A segunda implementacao do tipo la leva em consideracao o desempenho da implementacao.
Podemos construir um modulo, Queue, da seguinte forma:
module Queue (Queue, enqueue, dequeue, queueEmpty, newQueue) where
enqueue
:: t -> Queue t -> Queue t
dequeue
:: Queue t -> Queue t
queueEmpty :: Queue t -> Bool
newQueue
:: Queue t
data Queue t = Fila [t]
newQueue = (Fila [ ])
queueEmpty (Fila [ ]) = True
queueEmpty _
= 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) where
show (Fila [ ])
= .
show (Fila (x : xs)) = < ++ (show x) ++ (show (Fila xs))
A funcao dequeue retorna um par: o tem removido da la e a la restante, caso ela n
ao
esteja vazia. Se a la estiver vazia, uma mensagem de erro deve ser chamada para anunciar esta
excecao.
A denicao de dequeue usa um aspecto do casamento de padr
oes que ainda n
ao foi analisado
que e o padr
ao q@(Fila xs). O smbolo @pode ser traduzido por como, no casamento da
entrada. A vari
avel q casa com a entrada completa, Fila xs, de forma que xs d
a acesso `a lista
a partir da qual ela est
a sendo construda. Isto signica que podemos nos referir diretamente a`
entrada completa e a seus componentes na denicao. Sem esta exibilidade, a alternativa seria
dequeue (Fila xs)
|not (queueEmpty (Fila xs)) = (head xs, Fila (tail xs))
|otherwise
= error "A fila esta vazia"
onde a la original seria reconstruda a partir de xs. Esta forma de declaracao implica na
replicacao de xs, de tail xs de tail (tail xs), uma vez que, nas linguagens funcionais, n
ao
existem atualizacoes de celulas, ou seja, todas as vari
aveis sao, na realidade, constantes, porque
seus valores nao podem ser modicados. Isto signica que se x for declarado como tendo o valor
20, ele tera este mesmo valor ate o nal da execucao. Se outro valor for necessario, uma outra
vari
avel sera declarada contendo tal valor. Neste caso, xs tera que ser replicada porque outros
ponteiros podem existir para ela. Segundo [30], este e um problema das linguagens funcionais
e, na maioria dos casos, inevitavel.
O uso do padr
ao @ permite algum compartilhamento, evitando o desperdcio de memoria
com copias desnecessarias. Neste caso, a variavel q e acessada e ela e implementada como um
ponteiro para Fila xs, necessitando apenas celulas para q e n
ao para Fila xs.
135

Otimizando a implementa
c
ao
Em vez de adicionar elementos ao nal da lista, podemos adiciona-los no incio. Esta decis
ao
n
ao requer qualquer alteracao nas denicoes de newQueue e queueEmpty, mas teremos de
redenir enqueue e dequeue. Por exemplo,
enqueue x (Fila xs) = Fila (x : xs)
dequeue q@(Fila xs)
|not (queueEmpty q)
|otherwise

= (last xs, Fila (init xs))


= error "A fila esta vazia"

onde as funcoes last e init sao pre-denidas em Haskell. A funcao last retorna o u
ltimo elemento
de uma lista e init retorna a lista inicial, sem seu u
ltimo elemento.
Do ponto de vista da complexidade, vericamos que a funcao enqueue adicionava um elemento ao nal da lista e agora passou a faze-lo na cabeca. Era carae cou barata porque
tinha de percorrer toda a lista para adicionar o elemento ao seu nal, ou seja, dependia do
tamanho da lista. Por outro lado, com a funcao dequeue aconteceu exatamente o contrario:
era baratae cou cara, pelo mesmo motivo.
Uma otimizacao importante que se pode realizar nesta implementacao consiste em implementar as las 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, tambem
na cabeca (tambem barata), mas da primeira lista. Este esquema funciona desta maneira ate
que a primeira lista n
ao seja vazia. No momento em que ela se tornar vazia, mas ainde existirem
elementos na segunda lista, a primeira lista passa a ser a segunda com seus elementos em ordem
inversa e a segunda lista passa a ser a lista vazia. Neste caso, pode-se agora continuar o processo
de retirada de elementos da la, ate que as duas listas se tornem vazias. A nova implementacao
ca da seguinte forma:
data Queue a = Fila [a] [a]
emptyQ = Fila [ ] [ ]
queueEmpty (Fila [ ] [ ]) = True
queueEmpty _
= False
enqueue x (Fila xs ys) = Fila xs (x : ys)
dequeue (Fila (x : xs) ys)
dequeue (Fila [ ] ys)
dequeue (Fila [ ] [ ])

= (x, Fila xs ys)


= dequeue (Fila (reverse ys) [ ])
= error "A fila esta vazia"

Esta implementacao e substancialmente mais eciente que as implementacoes feitas atraves


de listas u
nicas.

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

setEmpty
inSet
addSet
delSet
pickSet

::
::
::
::
::

Set
(Eq
(Eq
(Eq
Set

t -> Bool
t) => t -> Set t -> Bool
t) => t -> Set t -> Set t
t) => t -> Set t -> Set t
t -> t

--testa se o conjunto eh vazio


--testa se um x estah em S
--coloca um item no conjunto
--remove um item de um conjunto
--seleciona um item de S

necessario testar a igualdade entre os elementos de um conjunto. Por este motivo, os


E
elementos do conjunto tem de pertencer `a classe Eq. Existem algumas implementacoes de
Set que exigem restricoes adicionais sobre os elementos do conjunto. Isto signica que podese construir uma Interface mais rica para Set incluindo as operacoes de uni
ao, intersecao e
diferenca de conjuntos, apesar delas poderem ser construdas a partir das operacoes denidas
nesta Interface.
module Set (Set, emptySet, setEmpty, inSet, addSet, delSet) where
emptySet
setEmpty
inSet
addSet
delSet
pickSet

::
::
::
::
::
::

Set
Set
(Eq
(Eq
(Eq
Set

t
t -> Bool
t) => t -> Set t -> Bool
t) => t -> Set t -> Set t
t) => t -> Set t -> Set t
t -> t

data Set t = S [t]

--listas sem repeticoes

emptySet = S [ ]
setEmpty (S [ ]) = True
setEmpty _
= False
inSet _ (S [ ])
= False
inSet 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 do
tipo b, implementando assim, uma funcao nita, com domnio em a e co-domnio b, atraves de
uma determinada estrutura de dados.
O tipo abstrato Table pode ter a seguinte implementacao:
137

module Table (Table, newTable, findTable, updateTable, removeTable) where


newTable
:: Table a b
findTable
:: (Ord a) => a -> Table a b -> Maybe b
updateTable :: (Ord a) => (a, b) -> Table a b -> Table a b
removeTable :: (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 [ ]) = Nothing
findTable x (Tab ((c,v) : cvs))
|x < c
= Nothing
|x == c = Just v
|x > c
= findTable x (Tab cvs)
updateTable
updateTable
|x < c =
|x == c =
|x > c =
removeTable
removeTable
|x < c =
|x == c =
|x > c =

(x, z) (Tab [ ]) = Tab [(x, z)]


(x, z) (Tab ((c,v) : cvs))
Tab ((x,z):(c,v):cvs)
Tab ((c,z):cvs)
let (Tab t) = updateTable (x,z) (Tab cvs)
in Tab ((c,v):t)
_ (Tab [ ]) = Tab [ ]
x (Tab ((c,v):cvs))
Tab ((c,v):cvs)
Tab cvs
let (Tab t) = removeTable x (Tab cvs)
in Tab ((c,v):t)

instance (Show a, Show b) => Show (Table a b) where


show (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 pode
ser implementado da seguinte forma:
module Main where
import Table
type Numero = Integer
type Nome
= String
type Nota
= Integer
pauta :: [(Numero, Nome, Nota)] -> Table Numero (Nome, Nota)
pauta [ ]
= newTable
pauta ((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

(6666,Margarida,26)]
Neste ponto, encerramos nosso estudo sobre os tipos abstratos de dados. Foram mostrados
v
arios exemplos de implementacao e esperamos ter dado uma ideia ampla da forma como estes
tipos podem ser implementadoe em Haskell. Na realidade os princpios subjacentes e que d
ao
suporte aos tipos abstratos de dados devem ser do domnio de todo programador, em qualquer
paradigma 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 baseados
neles. Como sugestao, indicamos a implementacao de arvores AVL, Red-black, B, B+, B* entre
outras.

5.8

Lazy evaluation

O mecanismo de avaliacao lazy j


a foi mostrado em v
arias oportunidades anteriores. No entanto,
ele foi analisado de forma supercial e agora chegou o momento de disseca-lo com maior profundidade, tentando aparelhar o leitor de ferramentas que o habilitem a tirar proveito deste
mecanismo engenhoso de avaliacao.
Um avaliador lazy avalia uma operacao apenas uma vez e se necessario. Isto tem inuencia
na Compreensao de listas e na construcao de listas potencialmente innitas. Para entender estas
inuencias, 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
eqFunc1 (9 - 3) (eqFunc1 34 (9 - 3))
= (9 - 3) + (eqFunc1 34 (9 - 3))
= 6 + (eqFunc1 34 6)
= 6 + (34 + 6)
= 6 + 40
= 46

--linha
--linha
--linha
--linha
--linha
--linha
--linha

1
2
3
4
5
6
7

Deve-se observar que, nas linhas 2 e 3 deste script, a subexpressao (9 - 3) ocorre duas vezes. Neste
caso, o avaliador de Haskell avalia esta subexpress
ao apenas uma vez e guarda este resultado
na expectativa de que ele seja novamente referenciado. Na segunda ocorrencia da mesma subexpressao, ela j
a esta avaliada. Outra caracterstica importante e que o sistema de avaliacao so
vai realizar um calculo se ele for realmente necessario. Como no -calculo, a ordem de avaliacao
e sempre da esquerda para a direita, ou seja, leftmost-outermost. Isto tem importancia na
implementacao: os calculos so serao realizados se realmente forem necessarios e no momento em
que forem solicidatos. Veja que na avaliacao da funcao eqFunc2, a seguir, o segundo argumento
(eqFunc1 34 3) n
ao e avaliado, uma vez que ele n
ao e necessario para a aplicacao da funcao
eqFunc2.
eqFunc2 a b
eqFunc2 (10
= (10 *
= 400 +
= 432

= a + 32
* 40) (eqFunc1 34 3)
40) + 32
32

Ser
ao vistas, a seguir, algumas implicacoes que esta forma de avaliacao tem sobre a construcao
de listas usando compreenssoes e sobre a construcao de listas potencialmente innitas.
139

5.8.1

Express
oes ZF (revisadas)

Ha uma forte interdependencia entre a criacao de listas por compreensao e o sistema de avaliacao
preguicosa de Haskell. Algumas aplicacoes que usam a construcao de listas por compreensao
so podem ser completamente entendidas se conhecermos o sistema de avaliacao, principalmente
quando envolver a criacao de listas potencialmente innitas.
Uma express
ao ZF tem a seguinte sintaxe:
[e | q1 , ..., qk ], onde cada qi e um qualicador e tem uma entre duas formas:
1. um gerador: p<- lExp, onde p e um padr
ao 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 u
ltimo exemplo, a ordem em que as triplas foram geradas. Ele pega o
primeiro n
umero do primeiro gerador, em seguida o primeiro gerador do segundo, depois percorre
todos os valores do terceiro gerador. No momento em que os elementos do terceiro gerador se
exaurirem o mecanismo volta para o segundo gerador e pega agora o seu segundo elemento e vai
novamente percorrer todo o terceiro gerador. Ap
os se exaurirem todos os elementos do segundo
gerador ele volta agora para o segundo elemento do primeiro gerador e assim prossegue ate o
nal.
Seq
u
encia de escolhas
Para mostrar a seq
uencia 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 x
sao substitudas 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 s
ao importantes para entender porque algumas aplicacoes, envolvendo a criacao de
listas, n
ao chegam aos resultados pretendidos.
[e | T rue, q2 , ..., qk ] = [e | q2 , ..., qk ],
[e | F alse, q2 , ..., qk ] = []
[e |] = [e].
Exemplos
Vamos mostrar a seq
uencia de operacoes para alguns exemplos para car mais claro:
140

[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)]
Exerccio. 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 n


umeros 1 a n. Ent
ao os seguintes passos serao
necessarios para resolver este problema:
construir a lista [1..n],
elevar `a quarta potencia cada n
umero 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 denida 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 n


ao e criada toda de uma s
o vez. Tao logo uma cabeca seja
criada, toma-se a sua quarta potencia e em seguida sera aplicada a soma com algum outro fator
que vai surgir em seguida.

5.8.3

Listas infinitas

Listas innitas nao tem sido utilizadas na maioria das linguagens de programacao. Mesmo nas
linguagens funcionais, elas s
o sao implementadas em linguagens que usam lazy evaluation. Na
realidade, estas listas nao sao innitas e sim sao potencialmente innitas. Vejamos um exemplo.
uns :: [Int]
uns = 1 : uns

-- a lista infinita de 1s [1, 1, ..]

somaPrimdoisUns :: [Int] -> Int


somaPrimDoisUns (a : b : x) = a + b
141

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 innita, o mecanismo de avaliacao n
ao precisa calcular toda a lista,
para depois somar apenas os seus primeiros dois elementos. Assim que o mecanismo encontra
estes 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]
Verique o que aconteceria se a ordem dos geradores fosse invertida!
2. O crivo de Erat
otenes. O crivo de Erat
ostenes e uma lista de inteiros criada a partir de
uma 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 do
crivo da lista que e feita retirando-se os m
ultiplos deste valor. Assim, crivo e denido da
seguinte forma:
crivo :: [Int] -> [Int]
crivo [ ] = [ ]
crivo (x : xs) = x : crivo [y | y <- xs, mod y x > 0]
3. A lista dos n
umeros primos. A lista innita dos n
umeros primos pode ser denida a patir
do crivo de Erat
ostenes denido anteriormente.
primos :: [Int] -> [Int]
primos = crivo [2..]
Exerccio
Dena listas innitas de fatorial e de Fibonacci.

5.9

Resumo

Este Captulo foi dedicado ao estudo de v


arios temas. No entanto o objetivo maior foi analisar
os tipos de dados complexos, em particular, os tipos algebricos e os tipos abstratos de dados. Foi
visto como eles podem ser construdos em Haskell e foram mostrados alguns exemplos para que
o 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, de
forma que estes modelos possam ser processados por um computador, emitindo solucoes para
os 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 construdas,
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 objetivo
do Captulo foi mostrar a grande gama de problemas em cujas solucoes a linguagem Haskell
pode ser aplicada com sucesso.
142

A grande fonte de exemplos e exerccios mostrados neste Captulo, foram os livros de Simon
Thompson [35] e Richard Bird [4]. No entanto, um fonte importante de problemas a serem
resolvidos 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 encontradas
na bibliograa dedicada aos temas Algoritmos e Estruturas de Dados. O leitor e aconselhado a
consultar.
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 da
multimdia usando Haskell.

143

144

Captulo 6

Programa
c
ao com a
c
oes em Haskell
... for exemple, a computation implying the modication of
a state for keeping track of the number of evaluatin steps
might be described by a monad which takes as input parameter and
returns the new state as part of its result.
Computations raising exceptions or performing input-output can
also be described by monads.
(Fethi Rabhi et Guy Lapalme in [30])

6.1

Introdu
c
ao

Este Captulo 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 atribudos
a nomes. Para Haskell, o resultado de um programa e o valor de nome main no M
odulo Main
do arquivo Main.hs. No entanto, os valores dos nomes sao imut
aveis durante a execucao do
programa, ou seja, o paradigma funcional n
ao admite atribuicoes destrutivas.
Mas a realidade e que a grande maioria dos programas exige alguma interacao com o mundo
externo. Por exemplo:
um programa pode necessitar ler alguma entrada de algum terminal ou escrever neste ou
em outro terminal,
um sistema de e-mail le e escreve em arquivos ou em canais e
um programa pode querer mostrar uma gura em uma janela do monitor.
Historicamente, as operacoes de I/O representaram um desao muito grande durante muito
tempo para os usu
arios das linguagens funcionais. Algumas delas tomaram rumos distintos na
solucao 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 padr
ao de entrada. Este
valor lido e atribudo a inputInt. Mas surge um problema porque, cada vez que inputInt e
avaliado, um novo valor e a ele tribudo. Esta e uma caracterstica do paradigma imperativo,
n
ao do modelo funcional. Por este motivo, diz-se que SML admite um modelo funcional impuro,
porque admite atribuicoes destrutivas.
Seja a seguinte denicao de uma funcao, em SML, que calcula a diferenca entre dois inteiros:
145

inputDif = inputInt - inputInt


Suponha que o primeiro tem de entrada seja 10 e o segundo seja 20. Dependendo da ordem
em que os argumentos de inputDif sejam avaliados, ela pode ter como resultado os valores 10
ou -10. Isto corrompe o modelo, uma vez que esperava-se o valor 0 (zero) para inputDif.
A razao deste problema e que o signicado de uma express
ao n
ao e mais determinado simplesmente pela observacao dos signicados de suas partes, porque n
ao podemos mais atribuir
um signicado a inputInt sem antes saber em que local do programa ele ocorre. A primeira e
a segunda ocorrencias de inputInt em inputDif podem ocorrer em diferentes tempos e podem
ter diferentes valores.
Um segundo problema com esta tecnica e que os programas se tornam extremamente difceis
de serem seguidos, porque qualquer denicao em um programa pode ser afetada pela presenca
de operacoes de I/O.
Por causa disto, durante muito tempo, as operacoes de I/O se tornaram um desao para as
linguagens funcionais e v
arias tentativas foram feitas na busca de solucoes que nao alterassem o
paradigma funcional.

6.2

Entrada e Sada em Haskell

Como ja descrito, um programa funcional consiste em uma expressao que e avaliada para encontrar um valor que ser
a ligado a um identicador. No caso de uma operacao de IO, que valor
deve 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 ser
executada 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, mas
tambem 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, cujos
retornos sejam acoes, tambem sao chamadas de comandos. Todos os comandos realizam acoes e
retornam 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/O
e retorne um valor do tipo a. Haskell tambem prove um tipo IO () que contem um u
nico
elemento, 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 de
I/O que n
ao retorna qualquer valor. Por exemplo, a operacao de escrever a string Olha eu
aqui!pode ser entendida desta forma, ou seja, um objeto do tipo IO ().
Existem muitas funcoes pre-denidas em Haskell para realizar acoes, alem de um mecanismo
para seq
uencializ
a-las, permitindo que alguma acao do modelo imperativo seja realizada sem
ferir o modelo funcional.

6.2.1

Opera
c
oes de entrada

Uma operacao de leitura de um caractere (Char), a partir do dispositivo padr


ao de entrada, e
descrita em Haskell pela funcao pre-denida getChar do tipo:
146

getChar :: IO Char
De forma similar, para ler uma string, a partir do dispositivo padr
ao de entrada, usamos a
funcao pre-denida getLine do tipo:
getLine :: IO String
As aplicacoes destas funcoes devem ser interpretadas como operacoes de leitura seguidas de
retornosi; no primeiro caso de um caractere e, no segundo, de uma string.

6.2.2

Opera
c
oes de sada

A operacao de impressao de um texto, e feita por uma funcao que toma a string a ser escrita
como entrada, escreve esta string no dispositivo padr
ao de sada e retorna um valor do tipo ().
Esta funcao foi citada no Captulo 3, mas de forma generica e sem nenhuma profundidade, uma
vez que, seria difcil o leitor entender sua utilizacao com os conhecimentos sobre Haskell, ate
aquele ponto, adquiridos. Esta funcao e putStr, pre-denida 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 denir uma funcao que escreva uma linha de sada:
putStrLn :: String -> IO ()
putStrLn = putStr . (++ "\n")
cujo efeito e adicionar o caractere de nova linha ao m 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 v
arios tipos, em strings para que possam ser mostradas
atraves da funcao putStr. Por exemplo, pode-se denir uma funcao de impressao geral
print :: Show a => a -> IO ()
print = putStrLn . show
Se o objetivo for denir uma acao de I/O que n
ao realize qualquer operacao de I/O, mas
que retorne um valor, pode-se utilizar a funcao
return :: a -> IO a
cujo efeito e n
ao realizar qualquer acao de I/O e retornar um valor do tipo a.
147

6.2.3

A nota
c
ao do

A notacao do e um mecanismo exvel, construdo para suportar duas coisas em Haskell:


1. a seq
uencializacao de acoes de I/O e
2. acaptura de valores retornados por acoes de I/O, para repassa-los futuramente para outras
acoes do programa.
Por exemplo, a funcao putStrLn str, descrita anteriormente, e pre-denida em Haskell e
faz parte do Prelude padr
ao de Hugs. Ela realiza duas acoes. a primeira e escrever a string str
no dispositivo padr
ao de sada e a segunda e fazer com que o prompt salte para a pr
oxima linha.
Esta mesma operacao pode ser denida 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 seq
uencializacao das acoes de I/O, em uma u
nica
acao. A sintaxe da notacao do e regida pela regra do oside e pode-se tomar qualquer n
umero
de argumentos (acoes).
Como outro exemplo, pode-se querer escrever alguma coisa n vezes. Por exemplo, podese querer fazer 4 vezes a mesma escrita do exemplo anterior. Uma primeira versao para esta
operacao pode ser o seguinte codigo em Haskell:
faz4vezes :: String -> IO ()
faz4vezes str = do putStrLn str
putStrLn str
putStrLn str
putStrLn str
Apesar de funcionar corretamente, esta declaracao mais parece com o metodo da forca
bruta. Uma forma bem mais elegante de descreve-la pode ser transforma a entrada da quantidade de vezes que se deseja que a acao seja realizada em um par
ametro.
fazNvezes :: Int -> String -> IO ()
fazNvezes n str = if n <= 1 then putStrLn str
else do putStrLn str
fazNvezes (n-1) str
Deve ser observada a forma de recursao na cauda utilizada na denicao da funcao fazNvezes,
simulando a instrucao de controle while, t
ao comum nas linguagens imperativas. Agora a funcao
faz4vezes pode ser redenida por
faz4vezes = fazNvezes 4
Apesar de terem sido mostrados apenas exemplos de sada, as entradas tambem podem ser
parte de um conjunto de acoes seq
uencializadas. Por exemplo, pode-se querer ler duas linhas do
dispositivo de entrada padr
ao e escrever a frase duas linhas lidas, ao nal. Isto pode ser feito
da seguinte forma:
leia2linhas :: IO ()
leia2linhas = do getLine
getLine
putStrLn "duas linhas lidas"
148

Capturando os valores lidos


No u
ltimo exemplo mostrado, foram lidas duas linhas mas nada foi feito com o resultado das
acoes de getLine. No entanto deve ser possvel 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 identicador linha parecer com uma variavel em uma linguagem imperativa, seu
signicado em Haskell e bem diferente.

6.3

Arquivos, canais e descritores

Os arquivos s
ao considerados como vari
aveis permanentes, cujos valores podem ser lidos ou
atualizados em momentos futuros, depois que o programa que os criou tenha terminada a sua
desnecessario comentar a import
execucao. E
ancia que estas variaveis tem sobre a computacao
e a necessidade de suas existencias. No entanto, e necessaria uma forma de comunicacao do
usu
ario com os arquivos. J
a foi vista uma forma, atraves do comando do. A outra, que ser
a
vista a seguir, e atraves de descritores.
Para obter um descritor (do tipo Handle) de um arquivo e necessaria a operacao de abertura
deste arquivo para que futuras operacoes de leitura e/ou escrita possam acontecer. Alem disso, e
necessaria uma operacao de fechamento deste arquivo, ap
os suas operacoes terem sido realizadas,
para que os dados que ele deve conter, n
ao sejam perdidos quando o programa de usu
ario
terminar sua execucao. Estas operacoes sao descritas em Haskell, da seguinte forma:
data IOMode = ReadMode | WriteMode | AppendMode | ReadWriteMode
openFile :: FilePath -> IOMode -> IO Handle
hClose :: Handle -> IO ()
Por convencao, todas as funcoes (normalmente chamadas de comandos) usadas para tratar
com 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 incluem
um caractere \n para a mudanca de linha ao nal da string.
Haskell tambem permite que todo o conte
udo de um arquivo seja retornado como uma u
nica
string, atraves da funcao
hGetContents :: Handle -> String
149

No entanto, h
a que se fazer uma observacao. Apesar de parecer que hGetContents retorna
todo o conte
udo de um arquivo de uma u
nica vez, n
ao e realmente isto o que acontece. Na
realidade, a funcao retorna uma lista de caracteres, como esperado, mas de forma lazy, onde os
elementos 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 = String
writeFile :: FilePath -> String -> IO ()
Tambem podemos acrescentar uma string ao nal de um arquivo com o comando
appendFile :: FilePath -> String -> IO ()
Ent
ao, qual a necessidade de se utilizar descritores? A resposta e imediata: eciencia. Vamos
analisar.
Toda vez que o comando writeFile ou appendFile e executado, deve acontecer tambem
uma seq
uencia de acoes, ou seja, o arquivo deve ser inicialmente aberto, a string deve ser escrita
e, nalmente, o arquivo deve ser fechado. Se muitas operacoes de escrita forem necessarias,
entao serao necessarias muitas destas seq
uencias. Ao se utilizar descritores, necessita-se apenas
de uma operacao de abertura no incio e outra de fechamento no nal.

6.3.2

Canais

Os descritores tambem podem ser associados a canais, que sao portas de comunicacao n
ao
associadas diretamente a um arquivo. Os canais mais comuns s
ao: a entrada padr
ao (stdin),
a area de sada padr
ao (stdout) e a area de erro padr
ao (stderr). As operacoes de IO para
caracteres e strings em canais incluem as mesmas listadas anteriormente para a manipulacao de
arquivos. Na realidade, as funcoes getChar e putChar sao denidas como:
getChar = hGetChar stdin
putChar = hputChar stdout
Ate mesmo hGetContents pode ser usada com canais. Neste caso, o m de um canal e
sinalizado com um cartactere de m de canal que, na maioria dos sistemas, e Ctrl-d.

6.4

Gerenciamento de excec
oes

Vamos agora nos reportar a erros que podem acontecer durante as operacoes de IO. Por exemplo,
pode-se tentar abrir um aquivo que ainda n
ao existe, ou pode-se tentar ler um caractere de um
arquivo que j
a atingiu seu nal. Certamente, n
ao se deve querer que o programa p
are por
estes motivos. O que normalmente se espera, e que o erro seja reportado como uma condicao
an
omala, mas que possa ser corrigida, sem a necessidade de que o programa seja abortado. Para
fazer este gerenciamento de excecoes, em Haskell, sao necessarios apenas alguns comandos de
IO.
As excecoes tem o tipo IOError. Entre as operacoes permitidas sobre este tipo, esta uma
colecao de predicados que podem ser usados para testar tipos particulares de excecoes. Por
exemplo,
150

isEOFError :: IOError -> Bool


detecta o m de um arquivo.
Mesmo assim, existe uma funcao catch que faz o gerenciamento de excecoes. Seu primeiro
argumento e a acao de IO que se esta tentando executar e seu segundo argumento e um descritor
de excecoes, do tipo IOError > IO a. Vejamos:
catch :: IO a -> (IOError -> IO a) -> IO a
Isto signica que no comando textbfcatch com ger, a acao que ocorre em com (pode ate gerar
uma seq
uencia longa de acoes, podendo ate mesmo ser innita) sera executada pelo gerenciador
ger. O controle e efetivamente transferido para o gerenciador atraves de sua aplicacao a` excecao
IOError. Por exemplo, esta versao de getChar retorna um caractere de uma nova linha, se
qualquer tipo de execucao for encontrada.
getChar :: IO Char
getChar = catch getChar (\e -> return \n)
No entanto, ele trata todas as excecoes da mesma maneira. Se apenas a excecao de m de
arquivo deve ser reconhecida, o valor de IOError deve ser solicitado.
getChar :: IO Char
getChar = catch getChar (\e -> if isEOFError e then return \n
else ioError e)
A funcao isError usada neste exemplo empurra a excecao para o pr
oximo gerenciador de
excecoes. 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 chamada
de dentro de uma seq
uencia de acoes normais ou a partir de um gerenciador de excecoes como
em getChar, deste exemplo.
Usando-se getChar, pode-se redenir getLine para demonstrar o uso de gerenciadores
aninhados.
getLine :: IO String
getLine = 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 Captulo nal deste trabalho, dedicado a` semantica de acoes, adotadas em Haskell,
para tratar operacoes de entrada e sada, representando a comunicacao que o programa deve
ter com perifericos e com os arquivos. Na realidade, ela e implementada em Haskell atraves de
M
onadas, uma teoria matematica bastante complexa e que, por este motivo, esta fora do escopo
deste estudo.
O objetivo do Captulo foi mostrar as formas como Haskell trata as entradas e as sadas de
dados, ou seja, que facilidades a linguagem Haskell oferece para a comunicacao com o mundo
151

externo ao programa. Isto inclui a leitura e escrita de dados em arquivos, bem como a criacao
e 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 Paul
Hudak [14]. Este ainda e considerado um tema novo pelos pesquisadores das linguagens funcionais e acreditamos ser este o motivo que o ele e ainda muito pouco tratado na literatura. Outra
possibilidade da ausencia de publicacoes nesta area pode ser o grau de diculdade imposto no
estudo dos M
onadas, que e considerado muito alto pela grande maioria dos pesquisadores da
area.
Com este Captulo, esperamos ter cumprido nosso objetivo inicial que foi o de proporcionar
aos estudantes iniciantes das linguagens funcionais, um pouco da fundamentacao destas linguagens e de suas aplicacoes, notadamente em programacao funcional usando Haskell, a linguagem
funcional mais em uso, no momento.

152

Bibliografia
[1] AHO, Alfred V; SETHI, Ravi et ULLMAN, Jerey D. Compilers, Principles, Techniques,
and Tools. 2nd. Edition. Addison-Wesley Publishing Company; 1988.
[2] ANDRADE, Carlos Anreazza Rego. AspectH: Uma Extens
ao Orientada a Aspectos de
Haskell. Dissertacao de Mestrado. Centro de Inform
atica. UFPE. Recife, Fevereiro 2005.
[3] BARENDREGT, H. P. em The Lambda Calculus: Its Syntax and Semantics. (Revised
Edn.). 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 Mem
oria em CMC. Dissertacao de
Mestrado. CIn-UFPE. Marco de 1994.
[9] DE SOUZA, Francisco Vieira. Teoria das Categorias: A Linguagem da Computac
ao.
Exame de Qualicacao. Centro de Inform
atica. UFPE. 1996.
[10] DE SOUZA, Francisco Vieira et LINS, Rafael Dueire. Aspectos do Comportamento Espacotemporal de Programas Funcionais em Uni e Multiprocessadores. X Simp
osio Brasileiro de
Arquitetura de Computadores e Processamento de Alto Desempenho. B
uzios-RJ. Setembro. 1998.
[11] DE SOUZA, Francisco Vieira et LINS, Rafael Dueire. Analysing Space Behaviour of
Functional Programs. Conferencia Latino-americana de Programacao Funcional. RecifePe. Marco. 1999.
[12] DE SOUZA, Francisco Vieira. Aspectos de Eciencia em Algoritmos para o Gerenciamento
Autom
atico Din
amico de Mem
oria. Tese de Doutorado. Centro de Inform
atica-UFPE.
Recife. Novembro. 2000.
[13] HINDLAY, J. Roger. Basic Simple Type Theory. Cambridge Tracts in Theorical Computer
Science, 42. Cambridge University Press. 1997.
[14] HUDAK, Paul. The Haskell School of Expression: Learning Funciotonal Programming
Through Multimedia. Cambridge University Press, 2000.
153

[15] HUGHES, John. Why Functional Programming Matters. In Turner D. T. Ed. Research
Topics in Funcitonal Programming. Addison-Wesley, 1990.
[16] JOHNSSON, T. Ecient Computation of Lazy Evaluation. Proc. SIGPLAN84. Symposium on Compiler Construction ACM. Montreal, 1984.
[17] JOHNSSON, T. Lambda Lifting: Transforming Programs to Recursive Equations. Aspen
as
Workshop on Implementation of Functional Languages. G
oteborg, 1985.
[18] JOHNSSON, T. Target Code Generation from G-Machine Code. Proc. Workshop on
Graph 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 University
of 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 Functional
Languages. J. Programming Languages 1:19-40; Chapmann & Hall. 1993.
[22] LINS, Rafael Dueire. O -C
alculo, Computabilidade & Lingugens de Programac
ao. Notas
de Curso. Recife-Pe, Nov. 1993.
[23] LINS, Rafael Dueire et all. Research Interests in Functional Programming. I Workshop
on Formal Methods. UFRGS. Outubro, 1998.
[24] MACLENNAN, Bruce J. Functional Programming Practice. Addison-Wesley Publishing
Company, Inc. 1990.
[25] MEIRA, Slvio Romero de Lemos. Introduc
ao `
a programac
ao Funcional. VI Escola de
Computacao. 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: Combinatorics 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 Programming
Context Training Manual. Texts in Computer Science. Springer Science+Business Media,
Inc. 2003.
[34] STOY, J. E. Denotational Semantics: The Scott-Strachey Approach to Programming
Language Theory. MIT Press, 1977.
154

[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. Software 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

Vous aimerez peut-être aussi