Escolar Documentos
Profissional Documentos
Cultura Documentos
LF Apostila
LF Apostila
USANDO HASKELL
Programao Funcional
Usando Haskell
UFPI/CCN/DIE
Teresina-Pi
Setembro de 2009
c
Copyright 2009,
c
Copyright 2009,
iii
Apresentao
Esta Apostila representa a compilao de vrios tpicos, desenvolvidos por pesquisadores de
renome, no campo da programao funcional. Ela tem como objetivo servir de guia aos profissionais de Informtica que desejam ter um conhecimento inicial sobre o paradigma funcional de
forma geral e, em particular, sobre programao usando Haskell. Ultimamente, Haskell tem se
tornado a linguagem funcional padro do discurso, j existindo vrios interpretadores e compiladores para ela, alm de vrias ferramentas de anlise de programas nela codificados (profiles).
Para atingir este objetivo, acreditamos que o estudo deva ser acompanhado de algum conhecimento, mesmo que mnimo, sobre a fundamentao destas linguagens e da forma como elas
so implementadas. Este conhecimento proporciona ao leitor uma viso das principais caractersticas e propriedades destas linguagens. Em particular, importante verificar que as tcnicas
utilizadas na compilao das linguagens imperativas no se mostraram adequadas na compilao
de linguagens funcionais porque o cdigo gerado sempre apresentou um desempenho abaixo da
crtica.
Em 1978, John Backus advogou o paradigma funcional como o que oferecia a melhor soluo
para a chamada crise do software. As linguagens funcionais so apenas uma sintaxe mais
cmoda para o -clculo. David Turner [47] mostrou, em 1979, que a lgica combinatorial poderia
ser extendida de forma a possibilitar a implementao eficiente de linguagens funcionais. Esse
trabalho provocou uma corrida em direo pesquisa nesta rea, gerando uma variedade de
tcnicas de implementao destas linguagens.
Dentre estas tcnicas, uma que tem sido adotada, com resultados promissores, a utilizao
do -clculo como linguagem intermediria entre a linguagem de alto nvel e a linguagem de
mquina. Os programas codificados em alguma linguagem funcional de alto nvel so traduzidos
para programas em -clculo e estes so traduzidos para programas em linguagem de mquina.
Neste caso, o -clculo desempenha um papel semelhante ao que a linguagem Assembly exerce,
como linguagem de montagem, na compilao de linguagens imperativas. Esta metodologia tem
dado certo, uma vez que j se conhecem tcnicas eficientes de traduo de programas em -clculo
para programas executveis, faltando apenas uma traduo eficiente de programas codificados em
uma linguagem funcional de alto nvel para programas em -clculo.
Esta Apostila tem incio em seu primeiro Captulo tratando das caractersticas das linguagens funcionais, destacando suas vantagens em relao s linguagens de outros paradigmas que
utilizam atribuies destrutivas. Em seguida, feita uma introduo ao -clculo. Apesar do
carter introdutrio, achamos ser suficiente para quem quer dar os primeiros passos em direo
aprendizagem desta tcnica. Os Captulos subseqentes se referem todos Programao Funcional usando Haskell.
Por ser uma primeira tentativa, a Apostila contm erros e sua apresentao didtico-pedaggica deve ser revista. Neste sentido, agradecemos crticas construtivas que sero objeto de anlise
e reflexo e, por isto mesmo, muito bem-vindas.
Teresina-Pi, setembro de 2009.
Francisco Vieira de Souza
E-mail: vieira.ufpi@gmail.com
iv
Contedo
Introduo
ix
1 Programao Funcional
15
1.1
Introduo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
15
1.2
Computabilidade de funes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
16
1.3
Anlise de dependncias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
17
1.4
18
1.4.1
19
1.4.2
Transparncia referencial . . . . . . . . . . . . . . . . . . . . . . . . . . . .
21
1.4.3
Interfaces manifestas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
23
Definio de funes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
23
1.5.1
. . . . . . . . . . . . . . . .
24
1.5.2
25
1.5.3
27
1.5.4
27
1.5.5
27
1.5.6
. . . . . . . . . . . . . . . . . . . . . . . .
28
1.5.7
28
Resumo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
31
1.5
1.6
2 -clculo
33
2.1
Introduo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
33
2.2
-expresses . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
34
2.3
A sintaxe do -clculo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
35
2.3.1
35
2.3.2
-abstraes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
36
2.3.3
36
36
2.4.1
38
2.4.2
Combinadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
39
41
2.4
2.5
2.5.1
-converso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
41
2.5.2
-converso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
41
2.5.3
-converso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
42
2.5.4
Convertibilidade de -expresses . . . . . . . . . . . . . . . . . . . . . . .
42
2.5.5
Captura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
43
2.5.6
Provando a conversibilidade . . . . . . . . . . . . . . . . . . . . . . . . . .
45
2.5.7
45
2.6
Ordem de reduo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
47
2.7
Funes recursivas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
49
2.8
Algumas definies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
51
2.9
Resumo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
52
3.3
3.4
3.5
Introduo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
53
3.1.1
Outras implementaes . . . . . . . . . . . . . . . . . . . . . . . . . . . .
54
Primeiros passos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
55
3.2.1
O interpretador ghci . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
57
3.2.2
Identificadores em Haskell . . . . . . . . . . . . . . . . . . . . . . . . . . .
58
Funes em Haskell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
59
3.3.1
Construindo funes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
59
3.3.2
. . . . . . . . . . . . . . . . . . . . . . .
61
3.3.3
62
63
3.4.1
63
3.4.2
Metodologias de programao . . . . . . . . . . . . . . . . . . . . . . . . .
69
3.4.3
72
3.4.4
Escopo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
73
3.4.5
Expresses condicionais . . . . . . . . . . . . . . . . . . . . . . . . . . . .
74
3.4.6
Clculos:
75
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Projeto de programas
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
77
Provas de programas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
78
Resumo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
81
3.5.1
3.6
53
4 O tipo Lista
83
4.1
Introdio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
83
4.2
84
4.3
88
4.4
91
4.5
97
vi
4.6
4.5.1
A funo map . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
98
4.5.2
Polimorfismo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
4.6.1
4.6.2
4.7
4.8
4.9
4.8.1
4.8.2
125
5.1
Introduo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
5.2
5.3
5.4
5.5
5.6
5.7
5.2.1
5.2.2
5.2.3
5.2.4
5.2.5
5.3.2
5.3.3
rvores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138
5.4.1
5.4.2
5.4.3
5.4.4
5.4.5
5.5.2
5.6.2
5.6.3
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156
vii
5.8
Resumo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
159
6.1
Introduo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
6.2
6.3
6.2.1
6.2.2
6.2.3
6.4
6.5
6.6
6.7
6.8
6.6.2
6.6.3
6.6.4
Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
6.7.1
6.7.2
Resumo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175
Introduo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177
7.1.1
7.2
7.3
177
. . . . . . . . . . . . . . . . . . . . . . 178
7.2.2
7.2.3
O comando do . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181
7.3.2
Canais . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186
7.4
7.5
Resumo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 192
Referncias Bibliogrficas
192
197
viii
205
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 206
ix
Introduo
Functional languages provide a framework
in which the crucial ideas of modern programming
are presented in the clearest possible way.
(Simon Thompson in [46])
Em 2004, Philip Wadler1 escreveu o artigo Why no one uses functional languages, onde ele
comenta sobre a pouca utilizao das linguagens funcionais na Indstria e ambientes comerciais
[50]. Para ele, dizer que ningum usa linguagem funcional, um exagero. As chamadas telefnicas no Parlamento Europeu so 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 utilizando 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 usurio. Alm disso, as linguagens funcionais so mais
adequadas construo de provadores de teoremas, incluindo o sistema HOL que foi utilizado
na depurao do projeto de multiprocessadores da linha HP 9000.
Ainda segundo Wadler, as linguagens funcionais produzem um cdigo de mquina com uma
melhoria de uma ordem de magnitude e, nem sempre, estes resultados so mostrados. Normalmente, se mostram fatores de 4. Mesmo assim, um cdigo que quatro vezes menor, quatro vezes
mais rpido de ser escrito ou quatro vezes mais fcil de ser mantido no pode ser jogado fora.
Para ele, os principais fatores que influenciam na escolha de uma linguagem de programao so:
Compatibilidade. Atualmente, os sistemas no so mais construdos monoliticamente,
como eram no passado. Os programas se tornaram grandes (programming in the large)
e agora eles devem ser escritos de forma modular por programadores em tempos e locais
possivelmente distintos, devendo serem ligados atravs de interfaces bem definidas.
necessrio acabar com o isolamento das linguagens funcionais e incorporar a elas facilidades para a comunicao entre programas funcionais e programas codificados em outras
linguagens pertencentes a outros paradigmas. A Indstria da Computao est comeando
a distribuir padres como CORBA e COM para suportar a construo de software a partir de componentes reutilizveis. Algumas linguagens funcionais j apresentam facilidades
para a construo de grandes softwares, com formas bem adequadas para a definio de
mdulos, apesar de algumas delas ainda no oferecem estas facilidades. Atualmente, os
programas em Haskell j podem ser empacotados como um componente COM e qualquer
componente COM pode ser chamado a partir de Haskell.
Bibliotecas. Muitos usurios escolheram Tcl, atrados, principalmente, pela biblioteca
grfica Tk. Muito pouco da atratividade de Java tem a ver com a linguagem em si, e
sim com as bibliotecas associadas, usadas na construo de grficos, banco de dados, interfaceamento, telefonia e servidores. Apesar de ainda no existirem muitas bibliotecas
1
Philip Wadler trabalha nos grupos de ML e de Unix na Bell Labs. Ele co-autor das linguagens Haskell e
GJ. Alm de vrios artigos publicados, ele tambm co-editor da revista Journal of Functional Programming.
grficas para as linguagens funcionais, muito esforo tem sido feito nesta direo, nos ltimos tempos. Haskell tem Fudgets, Gadgets, Haggis, HOpenGL e Hugs Tk. SML/NJ
tem duas: eXene e SML Tk. Haskell e ML tm, ambas, um poderoso sistema de mdulos
que tornam suas bibliotecas fceis de serem construdas, j tendo a biblioteca Edison com
estruturas de dados eficientes, construda por Okasaki [28] e mantida por Robert Dockins. Haskell ainda tem HSQL com interfaces para uma variedade de Bancos de Dados,
incluindo MySQL, Postgres, ODBC, SQLite e Oracle. Haskell ainda tem Happy, um gerador de parsers LALR, similar ao yacc, atualmente extendido para produzir parsers LR
para gramticas ambguas.
Portabilidade. Inegavelmente C e C++ tm sido preferidas em muitos projetos. No
entanto, muito desta preferncia no se deve ao fato de C gerar um cdigo mais rpido
que o cdigo gerado pelas linguagens funcionais, apesar de, normalmente, se verificar esta
diferena de desempenho. Na realidade, esta preferncia se deve mais portabilidade
inegvel de C. Sabe-se que os pesquisadores em Lucent preferiam construir a linguagem
PRL, para Banco de Dados, usando SML, mas escolheram C++, porque SML no estava
disponvel no mainframe Amdahl, onde deveria ser utilizada. Por outro lado, as tcnicas de
implementao de linguagens utilizando mquinas abstratas tm se tornado muito atrativas
para linguagens funcionais [22] e tambm para Java. Isto se deve muito ao fato de que
escrever a mquina abstrata em C a torna muito mais fcil de ser portada para uma grande
variedade de arquiteturas.
Disponibilidade. Alguns compiladores so muito difceis de serem instalados. Por exemplo, a instalao de GHC (Glasgow Haskell Compiler) era considerado uma aventura para
quem tentasse faz-lo. Ainda existem poucas linguagens funcionais comerciais e isto torna
difcil um cdigo estvel e um suporte confivel. Alm do mais, as linguagens funcionais
esto em permanente desenvolvimento e, portanto, esto sempre em transformaes. No
entanto, este quadro tem se modificado, pelo menos em relao a Haskell. Em 1998, foi
adotado o padro Haskell98 que permanece inalterado at o momento, apesar de continuar
a serem incorporadas novas extenses sua biblioteca. Atualmente, a instalao dos compiladores Haskell uma tarefa possvel de ser feita sem qualquer trauma estando disponvel
para vrias plataformas.
Empacotamento. Muitas linguagens funcionais seguem a tradio de LISP, de sempre
realizar suas implementaes atravs do loop read-eval-print. Apesar da convenincia,
essencial desenvolver habilidades para prover alguma forma de converso de programas
funcionais em programas de aplicao standalone. Muitos sistemas j oferecem isto, no
entanto, incorporam o pacote de runtime completo biblioteca e isto implica na exigncia
de muita memria.
Ferramentas. Uma linguagem para ser utilizvel necessita de ferramentas para depurao
e profiler. Estas ferramentas so fceis de serem construdas para linguagens estritas,
no entanto, so muito difceis de serem construdas para linguagens lazy, onde a ordem
de avaliao no conhecida a priori. Verifica-se uma exceo em Haskell, onde muitas
ferramentas de profiler j esto disponveis e muitas outras esto em pleno desenvolvimento.
Treinamento. Para programadores imperativos muito difcil programar funcionalmente.
Uma soluo imperativa mais fcil de ser entendida e de ser encontrada em livros ou
artigos. Uma soluo funcional demora mais tempo para ser criada, apesar de muito mais
elegante. Por este motivo, muitas linguagens funcionais atuais provem um escape para o
estilo imperativo. Isto pode ser verificado em ML que no considerada uma linguagem
funcional pura, porque permite atribuies destrutivas. Haskell uma linguagem funcional
2
pura, mas consegue imitar as atribuies das linguagens imperativas utilizando uma teoria
funcional complexa que a semntica de aes, implementadas atravs de mnadas2 .
Popularidade. Se um gerente escolher uma linguagem funcional para ser utilizada em
um projeto e este falhar, provavelmente ele ser crucificado. No entanto, se ele escolher C
ou C++ e no tiver sucesso, tem a seu favor o argumento de que o sucesso de C++ j foi
verificado em inmeros casos e em vrios locais. Este quadro tambm vem se modificando
em relao s linguagens funcionais, principalmente Haskell, onde ela tem se popularizado
muito nos ltimos anos.
Desempenho. H uma dcada atrs, 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 so melhores ou pelo menos esto
em ps de igualdade" com seus correspondentes em C. Isto depende da aplicao. Java
tem uma boa aceitao e, no entanto, seu desempenho muito inferior a C, na grande
maioria das aplicaes. Na realidade, existem linguagens com alto desempenho que no
so muito utilizadas e existem linguagens com desempenho mediano com alta taxa de
utilizao. Desempenho um fator importante, mas no tem se caracterizado como um
fator decisivo na escolha de uma linguagem.
Resumidamente, para ser bem utilizada, uma linguagem deve suportar trabalho interativo,
possuir bibliotecas extensas, ser altamente portvel, ter uma implementao estvel e fcil de ser
instalada, ter depuradores e profilers, ser acompanhada de cursos de treinamentos e j ter sido
utilizada, com sucesso, em uma boa quantidade de projetos.
Para o autor citado, todos estes requisitos j so perfeitamente atendidos por algumas linguagens funcionais, por exemplo Haskell. Para ele, o que ainda existe um preconceito injustificvel
por parte de alguns programadores de outros paradigmas de programao, apesar de que este
quadro tem se modificado vertiginosamente ao longo dos ltimos anos.
Na realidade, Haskell oferece aos usurios trs caractersticas principais: novidade, poder e
prazer [29]:
Novidade. Haskell, provavelmente, seja diferente de qualquer linguagem utilizada pelo
leitor. por oferecer um nova forma de pensar sobre software. Em Haskell, se enfatiza o
uso de funes que tomam valores imutveis como entrada e produzem novos valores como
sada, ou seja, sendo dadas as mesmas entradas, as funes devolvem sempre os mesmos
resultados.
Poder. Como um cdigo puro no tem envolvimento com o mundo externo, porque as
funes respondem apenas a entradas visveis, possvel se estabelecer propriedades sobre
seus comportamentos, que devem ser sempre verdadeiros, podendo, estes comportamentos, serem testados de forma automtica. Na linguagem tambm so utilizadas tcnicas
tradicionais para se testar cdigos que devem interagir com arquivos, redes ou hardware
exticos. No entanto, este cdigo impuro muito menor do que seria encontrado em um
programa codificado em uma linguagem tradicional. Isso aumenta a nossa confiana de
que o cdigo seja slido.
Prazer. Acredita-se ser fcil entender a programao em Haskell e isto torna possvel
construir programas pequenos e em um curto espao de tempo. Para se programar em
Haskell, so importadas muitas idias da Matemtica abstrata para o campo prtico, o que
tornam esta programao prazeirosa.
2
A semntica de aes um tema tratado no Captulo 7. Os mnadas no so aqui tratados, uma vez que seu
estudo requer, como pr-requsito, o conhecimento aprofundado sobre implementao de linguagens funcionais.
Resumidamente, Haskell uma linguagem que representa uma novidade, onde o leitor convidade a pensar em programao a partir de um ponto de vista diferente dos j experimentados,
representando uma nova perspectiva. Haskell uma linguagem poderosa, porque seus programas
so pequenos, rpidos e seguros. Finalmente, Haskell uma linguagem que proporciona prazer
em se programar com ela, porque em seus programas so aplicadas tcnicas de programao
adequadas para resolver problemas do mundo real.
Haskell na Indstria
Baseado em [29], sero mostradas a seguir alguns exemplos de grandes sistemas codificados em
Haskell:
Os projetos ASIC e FPGA (Lava, produtos da Bluespec, Inc.).
Software para composio musical (Hascore).
Compiladores e ferramentas relacionadas a eles (GHC).
Controle de reviso distribuda (Darcs).
Middlewere para Web (HAppS, produtos da Galois, Inc.).
Em termos de companhias que j utilizam Haskell em 2008, pode ser visto a partir da pgina
wiki (http://www.haskell.org/haskellwiki/Haskell_in_industry).
ABN AMRO. Um banco internacional que usa Haskell na avaliao de riscos e prottipos
de seus derivativos financeiros.
Anygma. Uma companhia que desenvolve ferramentas para a criao de multimdia.
Amgen. Uma companhia de biotecnologia que cria modelos matemtidos e outras aplicaes complexas.
Bluespec. Um projeto de software ASIC e FPGA.
Eaton. Usa Haskell no projeto e verificao de sistemas de veculos hidrulicos hbridos.
A nosso ver, o que existe mesmo a falta de informao e conhecimento do que realmente
programao funcional e quais as suas vantagens e desvantagens em relao programao em
outros paradigmas. Esta Apostila tenta prover subsdios para que seus usurios possam iniciar
um processo de discusso sobre o tema. Para desencadear este processo, vamos comear fazendo
uma comparao entre a programao funcional e a programao estruturada.
Este novo tipo de programao trouxe, como conseqncia, a necessidade de novos mtodos de
construo de software. Por serem grandes, os programas passaram a ser construdos, no apenas
por uma nica pessoa, mas por grupos ou times de pessoas, possivelmente trabalhando em locais
distintos. Foi necessrio desenvolver novos mtodos de trabalho em grupo que proporcionassem
a construo segura e eficiente destes softwares. A palavra chave passou a ser produtividade.
Este movimento ficou conhecido como a crise do software dos anos 80 e a soluo encontrada
para resolv-lo ficou conhecida como programao estruturada.
Para analisar as similaridades existentes entre a programao estruturada e a programao
funcional, faz-se necessrio conhecer os fundamentos nos quais se baseiam estas duas formas
de se construir programas. Vamos iniciar relembrando os princpios nos quais se baseiam a
programao estruturada. Hoare enumerou seis princpios fundamentais da estruturao de
programas [25]:
1. Transparncia de significado. Este princpio afirma que o significado de uma expresso,
como um todo, pode ser entendido em termos dos significados de suas sub-expresses.
Assim, o significado da expresso E + F depende simplesmente dos significados das subexpresses E e F, independente das complicaes de cada uma delas.
2. Transparncia de propsitos. Textualmente, Hoare argumenta: o propsito de cada
parte consiste unicamente em sua contribuio para o propsito do todo. Assim, em E
+ F, o nico propsito de E computar o nmero que ser o operando esquerdo do
operador +, valendo o mesmo para o operando F. Isto significa que seu propsito no
inclui qualquer efeito colateral.
3. Independncia das partes. Este princpio apregoa que os significados de duas partes,
no 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. Aplicaes recursivas. Este princpio se refere ao fato de que as expresses aritmticas
so construdas pela aplicao recursiva de regras uniformes. Isto significa que se ns
sabemos que E e F so expresses, ento sabemos que E + F tambm uma expresso.
5. Interfaces pequenas. As expresses aritmticas tm interfaces pequenas, porque cada
operao aritmtica tem apenas uma sada e apenas uma ou duas entradas. Alm disso,
cada uma das entradas e sadas um valor simples. Assim, a interface entre as partes
clara, pequena e bem controlada.
6. Estruturas manifestas. Este princpio se refere ao fato de que os relacionamentos estruturais entre as partes de uma expresso aritmtica seja bvio. Uma expresso uma
sub-expresso de uma expresso se estiver textualmente envolvida nela. Tambm duas
expresses no esto estruturalmente relacionadas se elas no se sobrepuzerem de alguma
forma.
Estas caractersticas so verdadeiras em relao programao estruturada, no entanto, nem
sempre elas so assim entendidas. A este respeito, John Hughes fez algumas reflexes, analisando a importncia da programao estruturada [13], fazendo um paralelo com as linguagens
funcionais. Em sua anlise, ele cita que quando se pergunta a algum o que programao
estruturada, normalmente se tem, uma ou mais, das seguintes respostas:
uma programao sem gotos,
uma programao onde os blocos no tm entradas ou sadas mltiplas ou
5
Esta uma resposta afirmativa, precisando apenas de um complemento para que ela fique
completa. Este complemento se refere s caractersticas que as linguagens funcionais apresentam
e que so responsveis por esta melhoria na modularidade dos sistemas. Estas caractersticas
podem ser sumarizadas na seguinte observao: a programao funcional melhora a modularidade, provendo mdulos menores, mais simples e mais gerais, atravs das funes de alta ordem
e lazy evaluation.
Estas duas caractersticas, verificadas apenas nas linguagens funcionais, que so responsveis
pela grande modularidade por elas proporcionada. Dessa forma, as linguagens funcionais so
altamente estruturadas e, portanto, representam uma soluo adequada para a to propalada
crise do software dos anos 80.
Para continuar esta viagem pelo mundo da programao funcional, tentando entender sua
fundamentao terica, necessrio conhecer tambm como as linguagens so implementadas.
Inicialmente, ser analisada a implementao de linguagens tradicionais e depois esta anlise se
particulariza para o caso da implementao das linguagens funcionais.
Problemas
Linguagemde
especificaoformal
Especificao
formal
Linguagemdeprogramao
dealtonvel
Programaem
linguagemdealtonvel
Biblioteca
Compilador
Programaem
linguagemdemquina
Entradade
dados
Resultados
duas etapas [1]. Na primeira delas, feita uma traduo do programa escrito em linguagem de
alto nvel para um programa codificado em linguagem intermediria, chamada de linguagem de
montagem (Assembly). Na etapa seguinte, feita a traduo do programa em linguagem de
montagem para o programa executvel, em linguagem de mquina. Este processo est mostrado
na Figura 2.
Esta mesma metodologia foi tentada por alguns pesquisadores na traduo de programas codificados em linguagens funcionais para programas em linguagem de mquina, mas os resultados
no foram animadores [23]. Os cdigos executveis gerados eram todos de baixo desempenho.
Por este motivo, os pesquisadores da rea de implementao de linguagens funcionais se viram
obrigados a buscar outras alternativas de compilao para estas linguagens.
8
Mquinas abstratas
Uma tcnica que tem sido usada com sucesso na traduo de programas codificados em linguagens
funcionais para programas codificados em -clculo de alto desempenho tem sido a transformao
das expresses iniciais em linguagens funcionais em expresses equivalentes em -clculo. Estas
-expresses iniciais, possivelmente, no apresentem um desempenho muito bom, mas sero
transformadas em -expresses mais simples, at atingir uma forma normal, se ela existir. O
resultado desta tcnica foi um sistema que ficou conhecido na literatura como mquinas abstratas.
A primeira mquina abstrata foi desenvolvida por Peter Landin em 1964 [20], que ganhou o
alcunha de mquina SECD, devido ao seu nome (Stack, Environment, Code, Dump).
4
-clculo uma teoria de funes que ser vista no Captulo 2 desta Apostila.
A mquina SECD usa a pilha S para a avaliao das -expresses Codificadas, utilizando o
ambiente E.
Uma otimizao importante na mquina SECD foi transferir alguma parcela do tempo de
execuo para o tempo de compilao. No caso, isto foi feito transformando as expresses com
notao infixa para a notao polonesa reversa que adequada para ser executada em pilha,
melhorando o ambiente de execuo. Para diferenciar da mquina SECD, esta mquina foi
chamada de SECD2.
Em 1979, David Turner [47] desenvolveu um processo de avaliao de expresses em SASL
(uma linguagem funcional) usando o que ficou conhecida como a mquina de combinadores.
Ele utilizou os combinadores S, K e I do -clculo5 para representar expresses, em vez de
-expresses, utilizando para isto um dos combinadores acima citados, sem variveis ligadas [8].
Turner traduziu diretamente as expresses em SASL para combinadores, sem passar pelo estgio
intermedirio das -expresses. A mquina de reduo de Turner foi chamada de Mquina de
Reduo SK e utilizava a reduo em grafos como mtodo para sua implementao. Esta
mquina teve um impacto muito intenso na implementao de linguagens aplicativas, pelo ganho
em eficincia, uma vez que ela no utilizava o ambiente da mquina SECD, mas tirava partido
do compartilhamento que conseguia nos grafos de reduo.
Um outro pesquisador que se tornou famoso no desenvolvimento de mquinas abstratas foi
Johnsson, a partir de 1984, quando ele deu incio a uma srie de publicaes [16, 17, 18] sobre este
tema, culminando com sua Tese de Doutorado, em 1987 [19], onde ele descreve uma mquina abstrata baseada em supercombinadores que eram combinadores abstrados do programa de usurio
para algumas necessidades particulares. A mquina inventada por Johnsson ficou conhecida pela
Mquina G e se caracterizou por promover uma melhoria na granularidade dos programas, que
era muito fina na mquina de reduo de Turner. Uma otimizao importante desta mquina foi
a utilizao de uma segunda pilha na avaliao de expresses aritmticas ou outras expresses
estritas.
10
A comunidade de Haskell
Existem muitas formas pelas quais leitores e usurios podem entrar em contacto com outros
programadores usurios de Haskell, a fim de tirar dvidas, saber o que outras pessoas esto
falando sobre o tema ou at mesmo apenas para manter contacto social. Podem ser citadas:
A principal fonte de recursos o site oficial de Haskell: http://www.haskell.org/. Apresenta
muitos links para vrias comunidades e atividades relacionadas ao tema.
Listas de discusso em Haskell: http://haskell.org/haskellwiki/Mailing_lists. O mais interessante o haskell-cafe, onde profissionais e admiradores de vrios nveis se encontram.
Para um chat de tempo real, o canal IRC Haskell: http://haskell.org/haskellwiki/IRC/channel.
nomeado por #Haskell.
Muitos grupos de usurios, workgroups acadmicos: http://haskell.org/haskellwiki/User_groups.
O jornal semanal de Haskell: http://sequence.complete.org/. Mostra um sumrio semanal
das atividades da comunidade de Haskell, com vrios links para listas de discusses.
O Haskell Communities and Activities Report: http://haskell.org/communities/. Coleciona informaes sobre pessoas que usam Haskell e o que elas esto fazendo.
Resumo
Este Captulo introdutrio foi feito na tentativa de apresentar a Programao Funcional como
uma alternativa importante na escolha de uma linguagem de programao, enumerando suas
principais caractersticas, mostrando suas principais vantagens sobre as outras. As vantagens
da programao sem atribuies em relao programao com atribuies so similares s da
programao sem gotos em relao programao com gotos. Resumidamente, so elas:
os programas so mais fceis de serem entendidos,
os programas podem ser derivados mais sistematicamente e
mais fcil de serem feitas inferncias sobre eles.
As vantagens da programao funcional sobre a programao imperativa podem ser resumidas
nos seguintes argumentos:
1. A programao funcional conduz a uma disciplina que melhora o estilo.
2. A programao funcional encoraja o programador a pensar em nveis mais altos de abstrao, atravs de mecanismos como funes de alta ordem, lazy evaluation e polimorfismo.
3. A programao funcional representa um paradigma de programao para a computao
massivamente paralela, pela ausncia de atribuies, pela independncia da ordem de avaliao e pela habilidade de operar estruturas complexas de dados.
4. A programao funcional uma aplicao da Inteligncia Artificial.
5. A programao funcional importante na criao de especificaes executveis e na implementao de prottipos com uma rigorosa fundamentao matemtica. Isto permite
verificar se as especificaes esto corretas, ou no.
11
O Captulo 7 dedicado aos tipos de dados abstratos. Ele tem incio com um estudo simplificado sobre a eficincia em programas funcionais. Em seguida, so mostrados os tipos de dados
abstratos e as formas como eles so construdos em Haskell. Todos os tipos abstratos mostrados so analisados quanto eficincia, tentando formas alternativas de constru-los levando em
considerao este fator. O Captulo termina, como todos os outros com um Resumo de seu
contedo.
O Captulo 8 dedicado s operaes de entrada e sada em Haskell, evidenciando o uso
de arquivos ou dispositivos como sada ou entrada de dados. Este processo em Haskell feito
atravs do mecanismo de aes, cuja semntica representa o contedo principal do Captulo.
A Apostila contempla as referncias bibliogrficas consultadas durante s sua elaborao juntamente com algumas outras indicaes que devem ser analisadas por quem deseja conhecer
melhor o paradigma funcional.
O Apndice A dedicado s principais funes j constantes do arquivo Prelude.hs e que
podem ajudar o usurio na construo de novas funes.
A Apostila termina com o Apndice B, dedicado compilao de programas codificados em
Haskell, utilizando o compilador GHC (Glasgow Haskell Compiler ) que gera cdigo nativo. Ele
se tornou necessrio na tentativa de tornar a Apostila autocontida, proporcionando ao leitor que
ele codifique e gere programas em Haskell e os compile para ddigo executvel.
13
14
Captulo 1
Programao 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 [8])
1.1
Introduo
A programao funcional teve incio antes da inveno dos computadores eletrnicos. No incio
do sculo XX, muitos matemticos estavam preocupados com a fundamentao matemtica; em
particular, queriam saber mais sobre os conjuntos infinitos. Muito desta preocupao aconteceu
por causa do surgimento, no final do sculo XIX, de uma teoria que afirmava a existncia de vrias
ordens de infinitos, desenvolvida por George Cantor (1845-1918) [25]. Muitos matemticos, como
Leopold Kronecker (1823-1891), questionaram a existncia destes objetos e condenaram a teoria
de Cantor como pura enrolao. Estes matemticos defendiam que um objeto matemtico s
poderia existir se, pelo menos em princpio, pudesse ser construdo. Por este motivo, eles ficaram
conhecidos como construtivistas.
Mas o que significa dizer que um nmero, ou outro objeto matemtico, seja construtvel ?
Esta idia foi desenvolvida lentamente, ao longo de muitos anos. Guiseppe Peano (1858-1932),
um matemtico, lgico e lingista, escreveu Formulaire de Mathmatique (1894-1908), onde
mostrou como os nmeros naturais poderiam ser construdos atravs de finitas aplicaes da
funo sucessor. Comeando em 1923, Thoralf Skolen (1887-1963) mostrou que quase toda
a teoria dos nmeros naturais poderia ser desenvolvida construtivamente pelo uso intensivo de
definies recursivas, como as de Peano. Para evitar apelos questionveis sobre o infinito, pareceu
razovel chamar um objeto de construtvel se ele pudesse ser construdo em um nmero finito de
passos, cada um deles requerendo apenas uma quantidade finita de esforo. Assim, nas primeiras
dcadas do sculo XX, j existia considervel experincia sobre as definies recursivas de funes
sobre os nmeros naturais.
A cardinalidade (quantidade de elementos) dos conjuntos finitos era fcil ser conhecida, uma
vez que era necessrio apenas contar seus elementos. Mas, para os conjuntos infinitos, esta
tcnica no podia ser aplicada. Inicialmente, era necessrio definir o que era realmente um
conjunto infinito. Foi definido que um conjunto era infinito se fosse possvel construir uma
correspondncia biunvoca entre ele e um subconjunto prprio de si mesmo. Foram definidos
os conjuntos infinitos enumerveis, caracterizados pelos conjuntos infinitos para os quais fosse
15
possvel construir uma correspondncia biunvoca com o conjunto dos nmeros naturais, N.
Assim, todos os conjuntos infinitos enumerveis tinham a mesma cardinalidade, que foi definida
por 0 1 . Assim, as cardinalidades do conjunto dos nmeros inteiros, Z, e dos nmeros racionais,
Q, tambm so 0 , uma vez que possvel construir uma correspondncia biunvoca entre Z e N
e entre Q e N. Os conjuntos infinitos em que no fosse possvel estabelecer uma correspondncia
biunvoca entre eles e N, foram chamados de infinitos no enumerveis.
Dado um conjunto A, o conjunto das partes de A, denotado por P(A), o conjunto cujos
elementos so todos os subconjuntos de A, incluindo ele prprio e o conjunto vazio. Um resultado
que garante a existncia de cardinais infinitos o fato de que o conjunto das partes de um conjunto
tem sempre cardinalidade maior que a deste. Este fato ficou conhecido como Teorema de Cantor,
aqui descrito sem demonstrao.
Teorema de Cantor. Seja A um conjunto e P(A) o conjunto das partes de A. Ento a
cardinalidade de A, denotada por #A, menor que a cardinalidade de P(A), ou seja, #A <
#P(A).
Prova-se que o conjunto das partes de N equipotente (tem a mesma cardinalidade) ao
conjunto dos reais, R. Considerando que 2k representa o cardinal do conjunto das partes de
conjuntos com cardinalidades k, ento 20 a cardinalidade do conjunto dos nmeros reais, R.
Mas, o que isto tem a ver com a Computao? Na realidade, prova-se que existem 0
programas que podem ser definidos em uma linguagem de programao como C, Java, Haskell,
ou outra. Alm disso, existem 20 funes de N para N. Logo, conclui-se que existem infinitas
funes que no podem ser representadas algoritmicamente, ou seja, que no so computveis.
Isto significa que a quantidade de funes computveis muito menor que a quantidade de
funes no computveis. Este um resultado, no mnimo, inusitado.
1.2
Computabilidade de funes
16
McCarthy sentiu-se atrado a usar funes recursivas e, alm disso, ele tambm achou conveniente
passar funes como argumentos para outras funes. McCarthy verificou que o -clculo provia
uma notao conveniente para estes propsitos e, por isto, resolveu usar a notao de Church
em sua pesquisa. Ainda em 1958, no MIT, foi iniciado um projeto com o objetivo de construir
uma linguagem de programao que incorporasse estas idias. O resultado ficou conhecido como
LISP 1, que foi descrita por McCarthy, em 1960, em seu artigo Recursive Functions of Symbolic
Expressions and Their Computation by Machine [25]. Neste artigo, ele mostrou como vrios
programas complexos podiam ser expressos por funes puras operando sobre estruturas de listas. Este fato caracterizado, por alguns pesquisadores, como o marco inicial da programao
funcional.
No final da dcada de 1960 e incio da dcada de 1970, um grande nmero de cientistas
da Computao comearam a investigar a programao com funes puras, chamada de programao aplicativa, uma vez que a operao central consistia na aplicao de uma funo a
seu argumento. Em particular, Peter Landin (1964, 1965 e 1966) desenvolveu muitas das idias
centrais para o uso, notao e implementao das linguagens de programao aplicativas, sendo
importante destacar sua tentativa de traduzir a definio de Algol 60, uma linguagem no funcional, para o -clculo. Baseados neste estudo, Strachey e Scott construiram um mtodo de
definio da semntica de linguagens de programao, conhecido como semntica denotacional.
Em essncia, a semntica denotacional define o significado de um programa, em termos de um
programa funcional equivalente.
No entanto, a programao aplicativa, que havia sido investigada por um reduzido nmero
de pesquisadores nos anos de 1960 e 1970, passou a receber uma ateno bem maior aps 1978,
quando John Backus, o principal criador do FORTRAN, publicou um paper onde fez severas
crticas s linguagens de programao convencionais, sugerindo a criao de um novo paradigma
de programao. Ele props o paradigma chamado de programao funcional que, em essncia,
a programao aplicativa com nfase no uso de funcionais, que so funes que operam sobre
outras funes. Muitos dos funcionais de Backus foram inspirados em APL, uma linguagem
imperativa projetada na dcada de 1960, que provia operadores poderosos, sem atribuio, sobre
estruturas de dados. A partir desta publicao, o nmero de pesquisadores na rea de linguagens
de programao funcional tem aumentado significativamente.
1.3
Anlise de dependncias
z = 2 a y + c;
onde tambm verificamos uma sub-expresso em comum (2 a y). Se for realizada a mesma
fatorao anterior, teremos:
t = 2 a y;
y = t + b; z = t + c;
Esta otimizao altera o valor da varivel z, porque os valores de y so diferentes nas duas
ocorrncias da sub-expresso 2 a y. Portanto, no possvel realizar esta otimizao. Apesar
da anlise de dependncia entre sub-expresses poder ser feita por um compilador, ela requer
tcnicas sofisticadas de anlise de fluxo. Tais anlises, normalmente so caras para serem realizadas e difceis de serem implementadas corretamente. No mundo das expresses, no existem
atualizaes destrutivas de variveis e, portanto, a varivel t no poderia ter mais de um valor.
De forma resumida, fcil e seguro realizar estas otimizaes nas expresses e difcil e inseguro de serem feitas no mundo das atribuies. Fica clara a vantagem das expresses sobre as
atribuies. O propsito da programao funcional extender as vantagens das expresses para
as linguagens de programao.
1.4
Vamos continuar analisando o mundo das expresses. Elas so estruturalmente simples porque
so compostas de aplicaes de operaes aritmticas a seus argumentos. Estas operaes so
funes puras, ou seja, so mapeamentos matemticos de entradas para sadas. Isto significa
que o resultado de uma operao depende apenas de suas entradas. Alm disso, uma expresso
construda a partir de funes puras e constantes tem sempre o mesmo valor. Por exemplo, seja
a funo pura f definida da seguinte forma:
f (u) = (u + b)(u + c)
em um contexto em que b = 3 e c = 2. A funo f pura porque definida em termos de
funes puras, que so as operaes aritmticas de adio e multiplicao. Vamos considerar a
avaliao de f (3ax) em um contexto em que a = 2 e x = 3. Como a avaliao de expresses
aritmticas independe da ordem de avaliao, assim tambm ser a avaliao de f . Isto significa
que o argumento (3ax) de f pode ser avaliado antes ou depois da substituio; o resultado ser
sempre o mesmo. Se ele for avaliado antes, a seqncia de avaliaes ser:
f (3ax) = f (3 2 3) = f (18) = (18 + b) (18 + c) = . . . = 21 16 = 336
Por outro lado, se a avaliao de 3ax for deixada para ser feita aps a substituio, a seqncia
de avaliaes ser:
18
1.4.1
Pelo que foi visto na seo anterior, para entender as vantagens do mundo das expresses e as
fontes de suas propriedades, necessrio investigar a ordem de avaliao nas expresses aritmticas. Avaliar alguma coisa significa encontrar seu valor. Assim, podemos avaliar a expresso
aritmtica 5 4 + 3"encontrando seu valor que, no caso, 23.
Podemos tambm avaliar a expresso (3ax + b)(3ax + c)? A resposta no; a menos que
sejam conhecidos os valores de a, b, c e x. Para entender como isto feito, vamos mostrar como
uma expresso avaliada, atravs da construo de uma rvore de avaliao, conforme pode ser
visto na Figura 1.1, onde as variveis ficam nas folhas e as operaes nos ns internos.
O valor desta expresso depende do contexto em que ela avaliada. Por exemplo, vamos
avali-la em um contexto em que a = 2, b = 3, c = 2 e x = 3. A avaliao pode ser iniciada em
vrios pontos mas, por motivo de regularidade, ela ser feita da esquerda para a direita. Nesta
estrutura, cada operao em um n interno depende apenas dos resultados das operaes dos
ns abaixo dele, na rvore. A avaliao de uma sub-rvore afeta apenas a rvore acima dela,
no influenciando os resultados das sub-rvores que estejam a sua esquerda ou a sua direita.
Colocando todos os operadores de forma explcita, a expresso se transforma em:
19
Uma expresso dita pura quando no realiza qualquer operao de atribuio, explcita ou implcita.
20
em qualquer ordem, sob demanda, podendo ser avaliados em paralelo. Cada operao depende
apenas de suas entradas que so os valores dos ns filhos. Este processo de avaliao pode ser
visto na Figura 1.2.
Este processo conhecido como decorao, onde cada n interno decorado com o valor
da aplicao da operao aos valores dos ns abaixo dele. A avaliao termina quando a raiz
da rvore for decorada com o valor final da expresso. Como afirmado anteriormente, diversos
processos podem acontecer em paralelo, na decorao da rvore, desde que seja observada a
estrutura de rvore. Isto significa que sempre se chega ao mesmo valor.
Esta propriedade verificada nas expresses puras, ou seja, a independncia da ordem de avaliao, chamada de propriedade de Church-Rosser. Ela permite a construo de compiladores
capazes de escolher a ordem de avaliao que faa o melhor uso dos recursos da mquina. A
possibilidade da avaliao ser realizada em paralelo, implica na utilizao de multiprocessadores
de forma bastante natural.
Por outro lado, as expresses impuras, normalmente, no apresentam esta propriedade,
conforme pode ser verificado no exemplo a seguir, em C. Seja a expresso a+2*fun(b). Ela
pura ou impura? Para responder a isso, devemos verificar a definio de fun. Por exemplo,
int fun(int x)
{
return(x * x);
}
Como fun no executa qualquer atribuio, a no ser a pseudo-atribuio a fun do valor
de retorno da funo, ela uma funo pura. Isto significa que, na avaliao da expresso
a+2*fun(b), pode-se avaliar primeiro a sub-expresso a ou 2*fun(b), que o resultado ser o
mesmo. No entanto, vamos supor a funo fun1 definida da seguinte forma:
int fun1(int x)
{
a = a + 1;
return (x * x);
}
Neste caso, fun1 uma pseudo-funo, porque ela no uma funo pura. Supondo que
a varivel a mencionada em fun1 seja a mesma varivel da expresso a+2*fun1(b), como
fun1 altera o valor de a, o valor de a+2*fun1(b) depende de qual operando do operador +
avaliado em primeiro lugar. Se, por exemplo, o valor de a for zero, caso ele seja avaliado primeiro
na expresso a + 2*fun1(b), o valor final da expresso ser 2b2 , ao passo que se 2*fun1(b)
for avaliado primeiro, a expresso final ter o valor 2b2 + 1.
Desta forma, para uma mesma linguagem, C, foram mostrados dois exemplos em que, no
primeiro, observa-se a independncia na ordem de avaliao e, no segundo, esta propriedade no
verificada.
1.4.2
Transparncia referencial
Vamos novamente considerar o contexto de avaliao da seo anterior. Se uma pessoa fosse
avaliar manualmente a expresso (3ax + b)(3ax + c) jamais iria avaliar a sub-expresso 3ax,
que neste contexto 18, duas vezes. Uma vez avaliada esta sub-expresso, o avaliador humano
substituiria a sub-expresso 3ax por 18, em todos os casos onde ela aparecesse. A avaliao seria
feita da seguinte forma:
21
t=3ax
=32x
=6x
=63
= 18
(18 + b) (18 + c)
(18 + 3) (18 + c)
21 (18 + (2))
21 16
336
Isto acontece porque a avaliao de uma mesma expresso, em um contexto fixo sempre dar
como resultado o mesmo valor. Para os valores de a = 2 e x = 3, 3ax ser sempre igual a 18.
Esta tcnica de avaliao humana tambm utilizada na avaliao de expresses puras. Seu
desenvolvimento pode ser entendido observando a rvore de avaliao da expresso mostrada na
Figura 1.3. Como a sub-expresso 3ax ocorre duas vezes, no existe razo para se duplicar a
representao na rvore. O valor desta sub-expresso compartilhado pelas operaes dos ns
que se encontram acima do n desta operao.
* =336
+ =21
+ =16
b=3
c=2
* =18
x=3
* =6
=3
a=2
fun1, da seo anterior, claro que, como fun1 deixa em a o registro do nmero de vezes que
ela chamada, no se poderia eliminar a sub-expresso comum, fun1(b), da expresso
(a+2*fun1(b))*(c+2*fun1(b))
Isto acontece porque a troca do nmero de vezes que fun1 chamada altera o resultado da
expresso. Em algumas linguagens de programao, isto complica a eliminao de sub-expresses
comuns.
1.4.3
Interfaces manifestas
A evoluo da notao matemtica ao longo dos anos tem permitido que ela seja utilizada,
com sucesso, para exibir muitas propriedades. Uma destas propriedades se refere s interfaces
manifestas, ou seja, as conexes de entradas e sadas entre uma sub-expresso e a expresso que
a envolve so visualmente bvias. Consideremos a expresso 4 + 5. O resultado desta adio
depende apenas das entradas para a operao (4 e 5) e elas esto mostradas de forma clara
na expresso, ou seja, uma est colocada esquerda e a outra direita do operador +. No
existem entradas escondidas para este operador.
Vamos imaginar agora a pseudo-funo fun2, definida da seguinte forma:
int fun2(int x)
{
a = a + 1;
return (a * x);
}
No existe uma forma de se conhecer o valor de fun2(5) sem antes saber o valor da varivel
no local, a. O valor de a atualizado em cada chamada a fun2, ento fun2(5) tem valores
diferentes em cada chamada. Esta situao caracteriza a existncia de interfaces escondidas para
a funo fun2, tornando difcil, ou mesmo impossvel, se prever o comportamento da funo.
Consideremos, novamente, a expresso (2ax + b) (2ax + c). As entradas para o primeiro
operador + (2ax e b) esto manifestas. O papel da sub-expresso 2ax + b, dentro da expresso
completa, tambm manifesto, ou seja, ela representa o argumento esquerdo do operador de
multiplicao. No existem sadas escondidas ou side effects na adio. As entradas so as
sub-expresses 2ax e b, em qualquer lado do operador e a sada facilmente calculada e liberada
para a expresso envolvente.
As caractersticas das interfaces manifestas podem ser resumidas da seguinte forma: as expresses podem ser representadas por rvores e a mesma rvore representa tanto a estrutura
sinttica de uma expresso quanto a forma como os dados fluem na expresso. As sub-expresses
que se comunicam entre si podem sempre ser colocadas em posies contguas na rvore ou na
forma escrita da expresso. Esta caracterstica no vlida no mundo das atribuies porque
as variveis alterveis permitem comunicao no local. Em geral, o grfico do fluxo de dados
no uma rvore e pode ser uma estrutura muito diferente da rvore sinttica. Assim, pode ser
impossvel colocar juntas as partes comunicantes, para que suas interfaces sejam bvias.
1.5
Definio de funes
1.5.1
Vamos considerar inicialmente as definies explcitas de variveis. Uma definio de uma varivel
explcita, em uma equao, se ela aparece no lado esquerdo desta equao e no aparece no
seu lado direito. Por exemplo, a equao
y = 2ax
define explicitamente a varivel y. As definies explcitas tm a vantagem de poderem ser interpretadas como regras de reescritas que informam como substituir diretamente uma expresso por
outra. Por exemplo, a definio anterior de y implica na regra de reescrita y 2ax informando
como eliminar a varivel y em uma frmula, onde y ocorra. Por exemplo, para eliminar y da
expresso 3y 2 + 5y + 1 aplica-se a regra de reescrita acima, para se obter
3y 2 + 5y + 1 3(2ax)2 + 5(2ax) + 1 12a2 x2 + 10ax
em que a varivel y no ocorre na expresso final. A definio explcita de variveis pode
ser extendida para conjuntos de equaes simultneas. Um conjunto de variveis definido
explicitamente por um conjunto de equaes, se
as variveis forem individualmente explcitas e
as equaes puderem ser ordenadas, de forma que nenhuma delas use em seu lado direito
uma varivel j definida anteriormente na lista.
Por exemplo, o conjunto de equaes
y =2ax
x=2
a=3
y 2ax
x2
a3
Estas regras podem ser aplicadas na seguinte ordem: a primeira delas aplicada at que no
exista mais y, em seguida a segunda aplicada at que no exista mais x e, finalmente, a terceira
aplicada at que no exista mais a. Desta forma teremos a seguinte seqncia de redues:
y 2 a x 2 a 2 2 3 2 6 2 12
Por outro lado, uma varivel definida implicitamente em uma equao se ela aparecer nos
dois lados desta equao. Por exemplo, a equao
24
2a = a + 3
define implicitamente a como 3. Para encontrar o valor de a necessrio resolver a equao
usando tcnicas da lgebra. O processo de soluo pode ser visto como uma forma de converter
uma definio implcita em uma definio explcita, mais usual, uma vez que uma definio
implcita no pode ser convertida diretamente em uma regra de reescrita. A equao anterior,
2a = a + 3, no informa explicitamente o que deve substituir a na expresso 2ax, por exemplo.
Para encontrar este valor necessrio utilizar as regras da lgebra, que podem no ser triviais.
Alm disso, as regras de reescrita que resultam de definies explcitas sempre terminam, ou seja,
a aplicao repetida das regras de reescritas elimina todas as ocorrncias da varivel definida.
No entanto, possvel escrever definies implcitas que no terminam, ou seja, nada definem.
Considere, como exemplo, a definio implcita
a=a+1
Apesar de sabermos que esta equao no tem soluo, este fato pode no ser to bvio, em
casos mais complexos. Se, ingenuamente, interpretarmos esta equao como a regra de reescrita
aa+1
ento chegaremos a um no determinismo na seguinte seqncia de redues:
2a 2(a + 1) 2((a + 1) + 1) . . .
As variveis tambm podem ser definidas implicitamente por conjuntos de equaes simultneas. Por exemplo, o conjunto de equaes
(
2a = a + 3
d 1 = 3d + a
2a = x
x+1=a+4
em que, nem a nem x aparecem nos dois lados de uma mesma equao, define implicitamente
a e x. Neste caso, no existe qualquer forma de ordenao destas equaes de maneira que
as ltimas equaes no faam uso de variveis j definidas nas equaes anteriores. A forma
implcita pode ser observada pela transformao das duas equaes em uma s, ou seja,
2a + 1 = a + 4
Em resumo, uma definio explcita de uma varivel informa o valor desta varivel, enquanto
uma definio implcita estabelece propriedades que apenas esta varivel deve apresentar. A
determinao do valor de uma varivel definida implicitamente exige um processo de soluo.
1.5.2
Aps termos analisado as definies explcitas e implcitas das variveis, vamos agora considerar
como elas so aplicadas ao caso das funes. Por exemplo, as duas equaes, a seguir, definem
implicitamente a funo implica.
and [p, implica (p, q)] = and (p, q)
and [not p, implica (p, q)] = or [not p, and (not p, q)]
Estas equaes no podem ser usadas explicitamente para avaliar uma expresso como
implica (T rue, F alse). Usando teoremas da lgebra booleana, estas equaes podem ser resolvidas para se chegar seguinte definio explcita:
25
1.5.3
1.5.4
1.5.5
As funes podem ainda ser compostas para formar uma nova funo. Como exemplo, a funo
implica pode ser definida da seguinte forma:
implica : Bool Bool Bool
27
1.5.6
Frequentemente, a definio de uma funo no pode ser expressa pela composio simples de
outras funes, sendo necessria a definio da funo para vrios casos. Por exemplo, a funo
que retorna o sinal algbrico de uma varivel pode ser definida da seguinte forma:
sinalg : Z
Z
se x > 0
1,
0,
se x = 0
sinalg(x) =
1, se x < 0
De forma similar, a diferena absoluta entre x e y pode ser definida da seguinte forma:
dif abs : Z Z( Z
x y, se x > y
dif abs(x, y) =
y x, se x y
1.5.7
Algumas situaes existem em que necessrio definir uma funo em termos de um nmero
infinito de casos. Por exemplo, a multiplicao de nmeros naturais () pode ser definida por
infinitas aplicaes da funo de adio (+). Seno vejamos:
0,
n,
mn=
se
se
n + n,
se
n + n + n, se
..
.
m=0
m=1
m=2
m=3
Como no possvel escrever um nmero infinito de casos, este mtodo de definio de funes
s usual se existir alguma regularidade ou algum princpio de unificao entre os casos, que
permita gerar os casos no definidos, a partir de casos j definidos. Se existir tal princpio de
unificao, ele deve ser estabelecido, sendo este o propsito das definies recursivas, onde um
28
objeto definido em termos de si prprio. Por exemplo, uma definio recursiva da multiplicao
anterior pode ser:
: N (
N N
0,
se m = 0
mn=
n + (m 1) n, se m > 0
Neste caso, uma avaliao de 2 3 feita por substituio da seguinte forma:
23 3+(21)3 3+13 3+3+(11)3 6+(11)3 6+03 6+0 6
A recurso o mtodo bsico de se fazer alguma operao iterativamente.
Exerccios resolvidos
1. Dados dois nmeros naturais, x e y, ambos maiores que zero, defina uma funo mdc(x,y)
que d como resultado o mximo divisor comum entre x e y.
Soluo:
mdc : N + N + N +
se x = y
x,
mdc(x y, y), se x > y
mdc(x, y) =
mdc(y, x),
se x < y
2. Dados dois nmeros naturais, x e y, ambos maiores que zero, defina uma funo div(x, y)
que d como resultado a diviso inteira de x por y.
Soluo:
div : N + N + N +
se x = y
1,
1 + div((x y), y), se x > y
div(x, y) =
0,
se x < y
3. Dados dois nmeros naturais, x e y, ambos maiores que zero, defina uma funo mod(x, y)
que d como resultado o resto da diviso de x por y.
Soluo:
mod : N + N + N +
se x = y
0,
mod((x y), y), se x > y
mod(x, y) =
x,
se x < y
4. Dados dois nmeros naturais, x e y, ambos maiores que zero, defina uma funo mmc(x, y)
que d, como resultado, o mnimo mltiplo comum entre x e y.
Soluo:
Temos que construir uma funo auxiliar, mmcaux(x, y, k), para algum k natural e k > 0,
de forma que k seja mltiplo de x e y. O menor valor de k ser o mximo entre os valores
de x e y.
mmcaux : N + N(+ N + N +
k,
se (mod(k, x) = 0)&(mod(k, y) = 0)
mmcaux(x, y, k) =
mmcaux(x, y, k + 1), senao
29
Agora pode-se definir o mnimo mltiplo comum usando esta funo auxiliar:
mmc : N + N + N +
mmc(x, y) = mmcaux(x, y, max(x, y))
onde a nova funo max, que retorna o maior valor entre dois nmeros naturais, pode
ser definida de forma simples. Deve ser observado que o mnimo mltiplo comum entre
dois nmeros tambm pode ser encontrado atravs da diviso do seu produto pelo mximo
divisor comum entre eles, ou seja, mmc(x, y) = div(x y, mdc(x, y)).
5. Dado um nmero natural n > 1, defina uma funo numdiv(n) que d como resultado o
nmero de divisores de n, incluindo o nmero 1.
Soluo:
De forma similar a que foi feita no exerccio anterior, ser construda uma funo auxiliar,
quantdiv(n, k), para algum k natural e k > 0.
quantdiv : N + N + N +
se n < 2k
0,
1 + quantdiv(n, k + 1), se mod(n, k) = 0
quantdiv(n, k) =
quantdiv(n, k + 1),
se mod(n, k) > 0
assim, a funo numdiv pode ser definida da seguinte forma:
numdiv : N + N +
numdiv(n) = quantdiv(n, 1)
Exerccios
1. Avalie a expresso 3 5, usando a definio recursiva da multiplicao mostrada nesta
seo.
2. Defina, recursivamente, em termos da multiplicao definida nesta seo, a funo potncia, onde uma base elevada a um expoente inteiro no negativo.
3. Defina, recursivamente, a exponenciao de nmeros no negativos elevados a uma potncia
inteira. Sugesto: use a definio condicional e a resposta do exerccio anterior.
4. Defina, recursivamente, a adio de inteiros no negativos, em termos das funes suc e
pred, onde suc(n)=n+1 e pred(n)=n-1, para n Z.
5. Defina, recursivamente, a adio e a subtrao de inteiros quaisquer, em funo das funes
pred e suc do exerccio anterior.
6. Dado um nmero natural n > 0, n dito perfeito se a soma de seus divisores, incluindo o
nmero 1, igual ao prprio n. O primeiro nmero natural perfeito o nmero 6, porque
6=1+2+3. Defina uma funo eperf eito(n) que informe se n , ou no, um nmero perfeito.
7. Dado um nmero natural n > 0, defina uma funo eprimo(n) que informe se n , ou no,
um nmero primo.
8. Considere o algoritmo a seguir que gera uma seqncia de nmeros naturais no nulos,
a partir de um nmero natural n > 0. Se n for par, divida-o por 2. Se n for mpar,
multiplique-o por 3 e some 1. Repita este processo com o novo valor de n, at que ele seja
igual a 1, se possvel. Por exemplo, para n = 22, a seqncia : 22, 11, 34, 17, 52, 26,
13, 40, 20, 10, 5, 16, 8, 4, 2 e 1. Para cada n, define-se o tamanho do ciclo de n como a
quantidade de nmeros da seqncia gerada, incluindo o nmero 1. No exemplo acima, o
tamanho do ciclo para n = 22 16. Defina uma funo tamciclo(n) que d como resultado
o tamanho do ciclo de n.
30
1.6
Resumo
Este Captulo foi dedicado fundamentao terica das linguagens funcionais, buscando inser-las
no contexto matemtico. Esta fundamentao tem como vantagem, primeiramente a constatao
de que essas linguagens tm um embasamenteo terico slido e depois, sendo este embasamento
oriundo da Matemtica, torna estas linguagens mais tratveis matematicamente e mais fceis de
terem seus programas provados.
A parte histrica mostrada no incio do Captulo foi baseada na Apostila de Rafael Lins [23]
e no livro de Brainerd [6]. No entanto, para uma viagem interessante pelos meandros da histria
da Matemtica o leitor deve consultar as referncias [5, 9].
A anlise da ligao existente entre as linguagens funcionais e a Matemtica foi, grande parte,
baseada no livro de Bruce Maclennan [25]. Ele adota a metodologia de praticar e depois justificar
a parte prtica atravs da teoria. Esta metodologia faz com que o livro seja uma referncia muito
interessante no apenas pelo mtodo utilizado mas, principalmente, pelo contedo muito bem
escrito. Outra referncia importante para este estudo foi o livro de Antony Davie [8] que apresenta
um contedo prximo da parte terica do livro de Maclennan. No menos importante, foi o livro
de Chris Okasaki [28] que apresenta uma teoria de linguagens funcionais bastante profunda e
objetiva.
31
32
Captulo 2
-clculo
Type theories in general date back
to the philosopher Bertrand Russell and beyond.
They were used in the early 1900s for the very specific
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 [10])
2.1
Introduo
Como foi mencionado na Introduo deste trabalho, o -clculo utilizado como linguagem
intermediria entre uma linguagem funcional de alto nvel e a linguagem de mquina. Desta
forma, o estudo do -clculo se torna importante para se compreender algumas construes dos
programas funcionais, sendo este o objetivo deste Captulo.
O -clculo foi desenvolvido por Alonzo Church no incio dos anos 30, como parte de um
sistema de lgica de ordem superior com o propsito de prover uma fundamentao para a
Matemtica, dentro da filosofia da escola logicista de Peano-Russel [23]. O -clculo uma
teoria que ressurge do conceito de funo como uma regra que associa um argumento a um valor
calculado atravs de uma transformao imposta pela definio da funo. Nesta concepo,
uma funo representada por um grafo, onde cada n um par (argumento, valor).
A lgica combinatorial foi inventada antes do -clculo, em cerca de 1920, por Moses Schnfinkel, com um propsito semelhante ao do -clculo [6]. No entanto, ela foi redescoberta 10 anos
depois, em 1930, por Haskell B. Curry, que foi o maior responsvel pelo seu desenvolvimento at
cerca de 1970. Em 1935, Kleene e Rosser provaram que o -clculo e a lgica combinatorial eram
inconsistentes, o que provocou um desinteresse acadmico pelo tema. No entanto, Curry no
desistiu de seu estudo e construiu uma extenso lgica combinatorial para fins de seus estudos, conseguindo sistemas mais fracos que a lgica clssica de segunda ordem. Em 1975, Dana
Scott e Peter Aczel mostraram que seu ltimo sistema apresentava modelos interessantes [6]. Em
1941, Church desistiu de sua pesquisa inicial e apresentou uma sub-teoria que lidava somente
com a parte funcional. Esta sub-teoria passou a ser chamada de -clculo puro e foi mostrada
ser consistente pelo teorema de Church-Rosser [23]. Usando o -clculo, Church props uma
formalizao da noo de computabilidade efetiva pelo conceito de definibilidade no -clculo.
Kleene mostrou que ser definvel no -clculo equivalente recursividade de Gdel-Herbrand.
Concomitantemente, Church formulou a sua conjectura associando a recursividade como a formalizao adequada do conceito de computabilidade efetiva. Em 1936, Allan Turing modelou a
33
computao automtica e mostrou que a noo resultante (computabilidade de Turing) equivalente definibilidade no -clculo. Desta forma, o -clculo pode ser visto como uma linguagem
de programao e como tal diversos tipos de problemas de programao podem ser analisados,
principalmente os relacionados com chamadas a procedimentos [23].
Curry e seus colegas [7], Barendregt [3] e outros desenvolveram extensivamente os aspectos
sintticos do -clculo, enquanto Scott [38], principalmente reportado por Stoy [45] e Schmidt
[37], se dedicou semntica da notao, o que veio a facilitar a vida dos usurios futuros do
-clculo.
2.2
-expresses
2.3
z
8
zy
(a . (ab))
(y . y) (a . (ab))
(a (z . y))a
(a . b . c . (ac)) (k . k)(x . x)
uma
uma
uma
uma
uma
uma
uma
varivel
constante
combinao
-abstrao
combinao
combinao
combinao
ou
ou
ou
ou
ou
aplicao ((z)(y))
funo
aplicao
aplicao
aplicao
A sintaxe do -clculo
Nesta seo ser mostrada a sintaxe do -clculo, ficando a semntica para uma prxima. A
sintaxe importante na construo de -expresses de forma correta.
2.3.1
Em sua forma mais pura, o -clculo no tem funes embutidas, como +, *, - e /. No entanto,
ser acrescentada uma coleo de tais funes como uma extenso ao -clculo puro, uma vez
que o nosso objetivo termos um -clculo que seja aplicvel.
Entre as funes embutidas que sero includas, citamos:
as funes aritmticas (+, -, *, /) e as constantes 0, 1, . . . e 9;
as funes lgicas (AND, OR, NOT) e as constantes TRUE e FALSE;
os caracteres constantes (a, b, ...);
a funo condicional IF: IF TRUE E1 E2 E1
IF FALSE E1 E2 E2
as funes CONS, HEAD e TAIL, onde
HEAD (CONS a b) a
TAIL (CONS a b) b;
a constante NIL, a lista vazia.
A escolha de funes embutidas arbitrria e, assim sendo, elas sero adicionadas medida
que as necessidades surgirem. Deve ainda ser mencionado que:
todas as aplicaes de funes so pr-fixas. Por exemplo, a expresso (+( 2 3) ( 8 2))
tem seus parnteses externos redundantes podendo, e at devendo, serem retirados para
evitar confuso visual;
do ponto de vista de implementao, um programa funcional deve ser visto como uma
-expresso que vai ser avaliada;
35
2.3.2
-abstraes
As funes embutidas no -clculo so formalizadas como j mostrado anteriormente. As abstraes so funes no embutidas e so construdas atravs do construtor (). Por exemplo,
(x . + x 1) uma -abstrao e deve ser interpretada da seguinte forma:
x
.
+x1
Uma -abstrao tem sempre estes 4 (quatro) elementos: o construtor (), o parmetro
formal (uma varivel, no caso, x) o ponto (.) e o corpo da funo (+ x 1). Uma -abstrao
pode ser comparada com as funes em uma linguagem de programao imperativa. Por exemplo,
a -abstrao anterior pode ser representada em C pelo seguinte fragmento de cdigo:
int inc (x)
int x;
{ return (x + 1);
Deve ser observado que as funes em uma linguagem de programao convencional tm,
obrigatoriamente, um nome, enquanto, no -clculo, as -abstraes so funes annimas.
2.3.3
2.4
A semntica operacional do -clculo diz respeito s regras utilizadas para converter uma expresso em outra. Na realidade, existem 3 (trs) regras de converso. Antes de serem mostradas
estas regras, devemos definir alguns termos que so utilizados na aplicao destas regras.
Uma idia central no estudo das linguagens de programao, da notao matemtica e da
lgica simblica a de ocorrncia livre e ocorrncia ligada (ou conectada) de uma varivel. Esta
36
idia, normalmente, provoca confuso em quem est vendo o tema pela primeira vez. Assim, ele
ser introduzido de forma gradual e intuitiva, utilizando exemplos j conhecidos da Matemtica.
Assim, vamos considerar o somatrio:
n
X
i2 + 1
i=1
Nesta expresso, a varivel i uma varivel ligada, ou seja, ela est fortemente atrelada
ao somatrio. Diz-se que ela ocorre ligada nesta expressso. Uma caracterstica importante
das variveis ligadas que elas podem ser renomeadas sem que o significado da expresso seja
alterado. Por exemplo, o somatrio anterior pode tambm ser representado da seguinte forma,
sem haver qualquer modificao em seu significado:
n
X
k2 + 1
k=1
x2 3xdx
t2 3tdt
Na teoria dos conjuntos, o conjunto de todos os x (ligada) tal que x 0 o mesmo conjunto
de todos os y (ligada) tal que y 0, ou seja,
{x | x 0} {y | y 0}
Tambm a proposio para todo x, x+1>x" equivalente proposio para todo y, y+1>y",
ou seja,
x[x + 1 > x] y[y + 1 > y]
Vejamos agora, uma outra expresso, tambm envolvendo somatrio:
i
n
X
(j 2 + i a)
j=1
J sabido que a ocorrncia da varivel j ligada. Isto pode ser verificado pelo fato de
que ela pode ser trocada por qualquer outra varivel, desde que no seja i ou a, sem mudar o
significado da expresso. Por exemplo, a expresso
i
n
X
(k 2 + i a)
k=1
em uma ocorrncia livre de uma varivel ela for trocada por outra, o significado da expresso
tambm modificado. Alm disso, uma varivel pode ocorrer ligada em uma expresso e livre
em outra. Por exemplo, a varivel i ocorre livre em
n
X
j2 + i a
j=1
i!
n
X
j 2 + i a
j=1
O escopo (binding site) de um identificador determina a regio da expresso na qual o identificador ocorre ligado. Esta regio, normamente, indicada por alguma conveno lxica, como
os parnteses, colchetes ou chaves. Esta regio o corpo da expresso, significando que uma
mesma varivel pode ocorrer ligada em um ponto e livre em outro.
Como visto, a troca de variveis ligadas no interfere no significado da expresso. No entanto,
esta troca no pode ser feita por uma varivel que ocorra livre na expresso, porque ela muda
seu significado. Por exemplo, a expresso que representa a soma dos elementos da linha i de uma
matriz Amn o somatrio
n
X
j=1
A varivel j ocorre ligada nesta expresso e pode ser trocada por outra, por exemplo, k.
n
X
k=1
2.4.1
Seja a -expresso (x . + x y) 4. Pelo que foi visto anteriormente, sabe-se que se trata da
aplicao de uma funo sobre a varivel x, onde o corpo da funo (+ x y), a uma outra
-expresso, que no caso a constante 4.
A varivel x pode ser pensada como um local onde o argumento 4 deve ser colocado. J para
a varivel y, este mesmo raciocnio no pode ser aplicado porque no existe este local sinalizado
por uma varivel y aps uma letra . Isto significa que as duas variveis (x e y) tm status
distintos.
38
Formalmente, define-se: uma ocorrncia de uma varivel ligada se existir uma -abstrao
qual esta varivel esteja ligada. Em caso contrrio, a ocorrncia livre. A Figura 2.1 mostra
um exemplo contendo ocorrncias livres e ligadas de variveis.
ligada
ligada
x.+(( y.+yz)7)x
^
ocorrencias:
livre
se x = y. Se x 6= y, a ocorrncia de x ligada.
se (x for livre em M ) E (x 6= y)
se (x for livre em M ) OU (x for livre em N )
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).
2.4.2
Combinadores
Uma -expresso que no apresenta variveis livres chamada fechada ou combinador. Existem
alguns combinadores que desempenham um papel especial no estudo do -clculo, conforme foi
afirmado na Introduo desta Apostila, onde foi feita referncia aos combinadores S, K e I, nos
quais baseada a mquina de reduo-SK de Turner.
A criao de combinadores uma tarefa que no tem explicao lgica. Seus construtores devem ter tido algum insight, no entanto no existem explicaes plausveis de alguma metodologia
de construo destas -expresses especiais.
Os combinadores mais conhecidos so:
I = x.x
Identidade
K = x.y.x
Projeo
S = x.y.z.xz(yz)
Composio
= x.xx
Duplicao
Y = f.(y.f (yy))(y.f (yy)) Usado na representao de funes recursivas
= a.b.b(aab)
Tambm usado na representao de funes recursivas
Exerccios resolvidos
1. Identifique nas expresses abaixo aquelas que so, ou no, -expresses:
a) a
Sim, a uma varivel;
b) 9
Sim, 9 uma constante;
c) ((b.b)(a.ab)
No, os parnteses no esto aninhados
corretamente;
d) (x.)a
No, x. no uma -abstrao;
e) ((x.y.y)(y.yyy))((i.i)(a.b)) Sim, porque contm apenas aplicaes,
abstraes e variveis.
39
Exerccios propostos
1. Justifique porque as expresses abaixo no so -expresses:
(a) (x(y)z)
(b) (x.y.x((i.ii)(b.c.b)(b.c.b))
(c)
(d) x.x
2. Identifique as ocorrncias livres e as ocorrncias ligadas das variveis nas expresses 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 expresses so combinadores?
(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))
40
2.5
2.5.1
-converso
1. (x. x 1) (y. y 1)
3. x.axa b.aba
A partir da definio de -reduo dada anteriormente, pode-se deduzir as seguintes relaes:
i)
ii)
iii) e =
(reflexo)
(simetria)
(transitividade)
Uma relao matemtica que reflexiva, simtrica e transitiva uma relao de equivalncia". Assim, a -reduo determina uma relao de equivalncia entre duas -expresses, o que
nos permite afirmar que estas duas -expresses representam a mesma entidade. Isto significa
que o usurio pode utilizar uma ou a outra, de acordo com a convenincia que cada caso requer.
2.5.2
-converso
1. (x . + x x) 5 + 5 5 = 10
2. (x . 3) 5 3
3. (x . (y . y x))4 5 (y . y 4)5 5 4 = 1
4. (x.xax)(x.x) (x.x)a(x.x)a(x.x)
2.5.3
-converso
Assim, elas tambm devem ser convertveis uma na outra, ou seja, (x . + 1 x) (+ 1).
2.5.4
Convertibilidade de -expresses
Diz-se que uma -expresso se -converte a uma outra -expresso , se existir uma seqncia
de -expresses < 1 , 2 , . . . , n >, onde
a) = 1 e n = e
2
4
3. Verifique se SK KI.
2.5.5
Captura
Deve ser tomado algum cuidado na escolha dos nomes dos parmetros, uma vez que mais de um
deles podem ter o mesmo nome. Por exemplo,
(x . (x . + ( x 1))x 3)9
(x . + ( x 1))9 3
linha 1
linha 2
+( 9 1)3
linha3
= + 8 3 = 11
linha 4
(x . (y . yx))y (x . (z . zx))y z . zy
43
O mesmo problema pode ser verificado na -expresso (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
z de (x . z . xz). Neste caso, uma possvel seqncia de redues seria (x . z . xz)(y . yz)
(y . + 5((x . x 3)y))6
+ 5 ((x . x 3)6)
+ 5 ( 6 3)
=+53
=8
2. As funes embutidas podem ser construdas como quaisquer outras. 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 seqncia de redues envolvendo estas duas funes:
HEAD (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 significa que no h a necessidade de que os construtores HEAD, CONS, TAIL ou
qualquer funo sejam pr-definidos. Na realidade, eles existem apenas por questo de eficincia.
Resumo
1. Troca de nomes: -converso permite trocar o nome de um parmetro de forma consistente.
2. Aplicao de funo: -converso permite aplicar uma -abstrao a um argumento,
construindo uma nova instncia do corpo da -abstrao.
3. Eliminao de -abstraes redundantes: -reduo pode, algumas vezes, eliminar abstraes redundantes.
44
2.5.6
Provando a conversibilidade
Muito freqentemente, nos deparamos com casos em que a prova de conversibilidade entre duas
-abstraes bastante complexa. Por exemplo, as -expresses IF T RU E ((p . p) 3) e
(x . 3) denotam a mesma funo, ou seja, a funo que sempre retorna o valor 3, independente
dos valores dos argumentos reais. Assim, espera-se que elas sejam conversveis uma na outra, j
que denotam a mesma funo. Realizando as -converses sobre a primeira, temos:
IF T RU E ((p . p) 3)
IF T RU E 3
(x . IF T RU E 3 x)
= (x . 3)
(x . 3) w
pela def de IF
2.5.7
A aplicao das regras de converso nem sempre to simples e direta como as mostradas nos
exemplos anteriores. Vamos introduzir agora uma notao bastante utilizada na converso de
-expresses, principalmente na implementao computacional de redutores, por ser bastante
intuitiva.
A notao E[M/x] significa que, na expresso E, todas as ocorrncias livres de x sero substitudas por M. Esta notao mostra explicitamente qual varivel est sendo trocada e por quem.
Deve ser notada a particularidade da notao E[M/x] que, apesar de representar o termo lido
como M por x" em portugus, representa a troca de x por M". Esta inverso de significado
deve-se to somente diferena de interpretao existente entre o portugus e o ingls, que foi o
idioma no qual a notao foi descrita.
Esta notao tambm pode ser utilizada em -converses. A Tabela 2.3 mostra um resumo
desta notao.
Exemplos
1. x[/x] =
2. x[/y] = x
45
-converso: (x . E) M E[M/x]
2.6
Ordem de reduo
Um redex (red uction ex pression) uma -expresso na qual todos os parmetros necessrios
para que uma operao possa ser feita esto prontos para serem utilizados. Se uma expresso
no contiver qualquer redex a sua avaliao est completa e, neste caso, diz-se que a expresso
est na sua forma normal.
No entanto, pode acontecer que uma expresso tenha mais de um redex e, neste caso, existem
mais de um caminho a serem seguidos na seqncia de avaliaes. J sabemos, pela propriedade
de Church-Rosser, que a utilizao de qualquer uma das seqncias de reduo leva ao mesmo
resultado, se ele existir. Por exemplo, + ( 3 4) ( 7 8) apresenta dois redexes: 3 4 e 7 8. Se
for escolhido o redex mais esquerda teremos a seguinte seqncia de redues:
+ ( 3 4) ( 7 8)
+ 12 ( 7 8)
+ 12 56
68
Se, no entanto, a escolha recair sobre o redex mais direita, a seqncia de reduo ser:
+ ( 3 4) ( 7 8)
+ ( 3 4) 56
+ 12 56
68
Apesar dos resultados das duas seqncias de avaliao serem iguais, algumas observaes
devem ser feitas:
1. nem toda -expresso tem uma forma normal. Por exemplo, a -expresso (), onde
= (x . x x), tem a seguinte seqncia de redues:
(x . x x) (x . x x) (x . x x) (x . x x) (x . x x) (x . x x) . . .
correspondendo a um loop infinito nas linguagens imperativas.
2. Algumas seqncias de reduo podem atingir a forma normal, enquanto outras no. Por
exemplo, (x . 3) () pode ser avaliada para 3, escolhendo o primeiro redex, mas, se
for escolhido o segundo ( aplicado a ), o resultado ser um loop infinito, sem atingir a
forma normal.
At este ponto, temos insistido, mesmo sem provas, que seqncias diferentes de redues
no podem levar a formas normais diferentes. Dois teoremas, descritos a seguir, mas sem demonstraes, garantem esta propriedade. Sugere-se ao leitor pesquisar estas demonstraes na bibliografia indicada, por no fazer parte do objetivo desta Apostila. Estas demonstraes requerem
conhecimentos matemticos e da Teoria da Computao mais avanados dos que os exigidos
como pr-requisitos para este estudo.
Teorema 1 de Churh-Rosser (CRT-I).Se E1 E2 , ento existe uma expresso E tal
que E1 E e E2 E".
Prova: Exerccio.
Corolrio. Nenhuma expresso pode ser convertida a duas formas normais distintas.
Prova: Suponha que uma -expresso, E, seja redutvel a duas -expresses distintas, E1 e E2
e que E1 e E2 estejam na forma normal. Pelo teorema CRT-1, anterior, existe uma -expresso
1 , tal que E 1 e E1 1 . Pelo mesmo teorema, tambm existe uma -expresso 2 , tal
47
X1
X
X2
(y . y)(z . z)
z . z
A escolha pela ordem normal de reduo garante encontrar a forma normal, se ela existir.
No entanto, isto no quer dizer que este seja o melhor mtodo a ser utilizado na reduo de expresses. Normalmente, este caminho o que apresenta o pior desempenho. Ento por que este
caminho deve ser seguido? A resposta que a ordem normal de execuo apresenta um mtodo
que pode ser implementado computacionalmente e isto o que se busca em nosso estudo, ou seja,
procura-se um mtodo para o qual exista um procedimento mecnico para segu-lo e enontrar a
forma normal, se ela existir. Na realidade, muitas otimizaes foram feitas buscando melhorar o
desempenho deste procedimento que, em ltima anlise, representam melhorias de desempenhos
na execuo de programas funcionais. Como exemplo de apenas uma destas otimizaes pode-se
citar David Turner [47].
2.7
Funes recursivas
partes da funo que no nos interessam, neste momento. Fazendo uma -abstrao em F AT ,
transformamo-la em F AT = (f . (n . (. . . f . . .))) F AT , sendo f uma varivel.
Esta funo pode ser escrita na forma F AT = HF AT onde, H = (f . (n . (. . . f . . .))).
Esta definio de H adequada aos nossos propsitos, uma vez que ela uma -abstrao
ordinria e no usa recurso. A equao F AT = H F AT estabelece que quando a funo H
aplicada a F AT o resultado o prprio F AT . Ento, F AT um ponto fixo de H.
Vamos agora procurar um ponto fixo genrico para H. Para isto vamos criar uma funo,
Y , que toma H como argumento e retorna um ponto fixo da funo, como resultado. Assim Y
deve ser tal que Y H seja um ponto fixo de H. Portanto, H(Y H) = Y H. Por este motivo, Y
chamado de combinador de ponto fixo. Se formos capazes de produzir tal combinador, nosso
problema estar resolvido.
Como Y H retorna um ponto fixo de H e F AT um destes pontos, ento F AT = Y H. Esta
definio no recursiva e atende s nossas necessidades. Para verificar isto, vamos computar
(F AT 1) utilizando as definies de F AT e de H, dadas anteriormente e que o mecanismo de
clculo obedece ordem normal de reduo ou seja, leftmost-outermost.
F AT = Y H
H = f . n . IF (= n 0) 1 ( n(f ( n 1)))
Ento
FAT 1
= YH 1
= H (Y H) 1
= (f . n . IF (= n 0) 1 (* n(f (- n 1))) (Y H) 1
(n . IF (= n 0) 1 (* n ((Y H) (- n 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 definio de IF
=1
A forma como o combinador Y definido j foi mostrada anteriormente, no entanto ele ser
novamente visto aqui, usando apenas algumas renomeaes de variveis:
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))
50
2.8
Algumas definies
Para terminar este Captulo, vamos deixar algumas definies de funes e de outros objetos.
Por exemplo, os nmeros naturais podem ser definidos como -expresses, conhecidas como os
numerais de Church. Deve ser observado que algumas definies utilizam o caractere $ quando
referenciam outras definies, para diferenciar de alguma varivel.
1. F = f.n.$iszero n $one ($mul n (f ($pred n)))
2. Fv = f.n.$iszero n (d.$one)(d.$mul n (f ($pred n))) (d.d)
3. I = x.x
4. K = x.y.x
5. = (x.x x)(x.x x)
6. S = f.g.x.f x (g x)
7. Y = h.(x.h(x x))(x.h(x x))
8. YT = (x.y.y (x x y)) (x.y.y (x x y))
9. Yp = h.(x.h (a.x x a)) (x.h (a.x x a))
10. Yv = h.(x.a.h (x x)a) (x.a.h (x x)a)
11. add = m.n.f.x.m f (n f x)
12. append = $Y (g.z.w.$null z w ($cons ($hd z) (g ($tl z) w)))
13. cons = x.y.$pair $false ($pair x y)
14. exp = m.n.f.x.m n f x
15. false = x.y.y
16. fst = p.p $true
17. hd = z.$fst ($snd z)
18. iszero = n.n (v.$false) $true
19. mul = m.n.f.x.m (n f )x
20. next = p.$pair ($snd p) ($succ ($snd p))
21. nil = z.z
22. null = $fst
23. pair = x.y.s.s x y
24. pred = n.$fst (n $next ($pair $zero $zero))
25. snd = p.p $false
26. succ = n.f.x.f(n f x)
27. tl = z.$snd ($snd z)
51
2.9
Resumo
Este Captulo foi todo dedicado ao estudo do -clculo. Ele se fez necessrio dada a importncia
que esta teoria tem na fundamentao das linguagens funcionais. Seu papel semelhante ao
desempenhado pela linguagem Assembly na traduo de programas em linguagens imperativas
para o cdigo de mquina. No entanto, deve ser salientado que esta teoria matemtica no
to simples como aqui parece. Esta abordagem simples foi adotada, dado o carter introdutrio
exigido para se entender como o -clculo utilizado na compilao de linguagens funcionais.
Quem quizer se aprofundar neste tema deve consultar a bibliografia especfica. As notas
de aula de Rafael Lins [23] e de Peter Welch [51] representam um bom comeo, dada a grande
quantidade de exerccios indicados e resolvidos. Quem estiver interessado em detalhes mais aprofundados sobre a implementao do -clculo deve consultar o livro de Peyton Jones [32]. Uma
abordagem mais terica desta linguagem, pode ser encontrada nos livros de Bruce MacLennan
[25] e de Antony Davie [8].
52
Captulo 3
3.1
Introduo
Os Captulos anteriores foram feitos com o objetivo de servirem como preparao e fundamentao para o estudo das linguagens funcionais, em particular, de Haskell. Este Captulo e os
seguintes so todos dedicados codificao de programas funcionais utilizando esta linguagem.
A comunidade das linguagens funcionais tem dado a Haskell uma ateno especial e, por este
motivo, muita pesquisa tem sido feita tentando dot-la de caractersticas que a torne uma linguagem de uso popular. Estas caractersticas foram citadas por Philip Wadler [50] e analisadas
na Introduo desta Apostila.
A histria de Haskell, desde a sua concepo at seu estado atual, est bem documentada
em um artigo escrito por Paul Hudak, John Hughes, Simon Peyton Jones e Philip Wadler [12].
Estes autores, alm de outros, participaram de todo o processo de criao e desenvolvimento da
linguagem e, por este motivo, contam com detalhes toda a trajetria de Haskell, desde a sua
concepo em Setembro de 1987, em Portland, no Oregon.
Haskell uma linguagem funcional pura, no estrita, fortemente tipada, cujo nome uma
homenagem a Haskell Brooks Curry, um estudioso da lgica combinatorial e um dos mais proeminentes pesquisadores sobre -clculo [4, 46]. uma linguagem baseada em scripts, que consistem
em um conjunto de definies associadas a nomes, em um arquivo.
A primeira edio do Haskell Report, verso 1.0, foi publicada em 1 de abril de 1990 por Paul
Hudak e Philip Wadler com o bjetivo de ser usada como fonte de pesquisa em projetos de linguagens de programao, ou seja, desejava-se que a linguagem estivesse sempre em evoluo. No entanto, Haskell comeou a se tornar popular e isto obrigou seus projetistas a repensarem o objetivo
inicial e resolveram nomear Haskell 98" como uma instncia de Haskell que deveria ser estvel
o bastante para que textos e compiladores pudessem ser construdos sem muita mudana[12]. A
partir de ento, Haskell98 passou a ser considerada a verso oficial a ser utilizada at a definio
de Standard Haskell. No entanto, a linguagem continua sendo pesquisada buscando a criao de
novas Bibliotecas a serem incorporadas ao sistema. Tambm muitas extenses esto sendo incorporadas, como Haskell concorrente, IDLs (Interface Description Language), HaskellDirect
e interfaces para C e C++, permitindo integrao com estas e outras linguagens. Em particular,
53
tem sido desenvolvida AspectH, uma extenso de Haskell para suportar orientao a aspectos
[2], e HOpenGl, uma extenso para OpenGL, entre outras.
O site oficial na WEB sobre Haskell http://www.haskell.org, onde muitas informaes
podem ser encontradas, alm de vrios links para compiladores e interpretadores para ela. Para
produzir cdigo executvel de mquina, foram desenvolvidos vrios compiladores.
Na Universidade de Glasgow, foi construdo GHC (Glasgow Haskell Compiler ) disponvel
para ambientes UNIX (Linux, Solaris, *BSD e MacOS-X) e tambm para Windows. GHC foi
iniciado em Janeiro de 1989 e , provavelmente, o compilador mais completo em atividade,
um projeto open-source com licena liberal no estilo BSD. Sua primeira verso foi escrita por
Kevin Hammond em LML, cujo prottipo ficou pronto em Junho de 1989, quando um time de
pesquisadores da Universidade de Glasgow constitudo por Cardelia Hall, Will Partain e Peyton
Jones resolveram reimplement-lo em Haskell (bootstrapping), com um parser construdo em
Yacc e em C. Esta implementao ficou pronta no outono de 1989 [12]. GHC est disponvel em
http://www.dcs.gla.ac.uk/fp/software/ghc/
Na Universidade de Chalmers, Lenhart Augustsson implementou Haskell em LML, um compilador que ficou conhecido como hbc, uma referncia a Haskell Brooks Curry, o pesquisador
a quem a linguagem deve o nome. HBC est disponvel em http://www.cs.chalmers.se/ augustss/hbc.html.
Tambm est disponvel o compilador nhc, considerado fcil de ser instalado, com heap
profiles, muito menor que os outros congneres, disponvel para todos os padres UNIX e
escrito em Haskell 98. O compilador nhc foi originalmente desenvolvido por Niklas Rgemo, um
aluno de Doutorado na Universidade de Chalmers [35]. Sua motivao inicial foi construir um
compilador mais eficiente em termos de espao e memria que os sistemas GHC e hbc. Para isto
ele utilizou muitas ferramentas de heap-prifiling que haviam sido desenvolvidas em York e que
revelavam muito desperdcio de memria em hbc. Para isto, ele resolveu fazer Ps-Doutorado em
York em colaborao com Colin Runciman, onde desenvolveram vrios mtodos de heap-profiling
para detectar ineficincias de espao, produzindo uma verso muito eficiente de nhc.
3.1.1
Outras implementaes
3.2
Primeiros passos
Existem duas formas nas quais um texto considerado um programa em Haskell. A primeira delas
considerar todo o texto como um programa, exceto o que comentado, que pode ser de duas
maneiras: com - -, que representa um comentrio at o final da linha corrente, ou envolvendo o
comentrio com os smbolos {- e -}, podendo englobar vrias linhas. Os arquivos em Haskell
com este tipo de programa devem ter a extenso .hs. Esta a maneira mais utilizada pelos
programadores de Haskell.
A outra forma considerar todo o texto como comentrio e sinalizar o que deve ser realmente
um programa iniciando a linha com o sinal > identado. Neste caso, o arquivo deve ter extenso
.lhs. Vamos mostrar um exemplo de cada situao.
{- ######################################################################
exemplo.hs
Este arquivo eh um exemplo de um arquivo .hs. Deve ser editado como arquivo
texto e salvo com a extensao .hs.
#########################################################################-}
valor :: Int -- Uma constante inteira
valor = 39
55
novalinha :: Char
novalinha = \n
resposta :: Bool
resposta = True
maior :: Bool
maior = (valor > 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 viso de literal, com a extenso .lhs. Deve
ser observado que os sinais de incio e fim de comentrios desaparecem.
##########################################################################
exemplo.lhs . Este arquivo eh um exemplo de um arquivo .lhs.
##########################################################################
> valor :: Int -- Uma constante inteira
> valor = 39
> novalinha :: Char
> novalinha = \n
> resposta :: Bool
> resposta = True
> maior :: Bool
> maior = (valor > 71)
> quadrado :: Int -> Int
> quadrado x = x*x
> todosIguais :: Int -> Int -> Int -> Bool
> todosIguais n m p = (n == m) && (m == p)
#########################################################################
A identao em Haskell importante. Ele usa um esquema chamado layout para estruturar
seu cdigo [36], a exemplo de Python. Este esquema permite que o cdigo seja escrito sem a
necessidade de sinais de ponto e vrgula explcitos, nem de chaves.
Neste caso, a identao do texto tem um significado bem preciso, descrito em trs regras
funcamentais:
se uma linha comea em pelo menos uma coluna frente (mais direita) do incio da linha
anterior, considerada a continuao da linha precedente,
56
se uma linha comea na mesma coluna que a do incio da linha anterior, elas so consideradas definies independentes entre si e
se uma linha comea em pelo menos uma coluna anterior (mais esquerda) do incio da
linha anterior, ela no pertence mesma lista de definies.
3.2.1
O interpretador ghci
Comando
:?
:e
:e exemplo.hs
:l exemplo.hs
:a exemplo.hs
:q
Para o usurio executar qualquer funo do Prelude necessrio apenas chamar esta funo
na linha de comandos (prompt), disponvel aps o interpretador ser carregado, seguida de seus
argumentos e apertar a tecla Enter. O resultado desta execuo exibido imediatamente
na linha seguinte ao prompt". Assim, o interpretador tambm pode ser usado como uma
calculadora para avaliar expresses aritmticas, booleanas, logartmicas, trigonomtricas, etc. A
forma de utilizao a mesma j citada. Algumas destas funes ou operadores aritmticos esto
mostrados na Tabela 3.2.
Tabela
Prioridade
9
8
7
6
5
4
3
2
1
0
3.2.2
Identificadores em Haskell
else
hiding
if
import
in
infix
infixl
infixr
instance
let
module
of
renaming
then
to
58
type
where
3.3
Funes em Haskell
Int
+
Int
Int
Int
Int
todosIguais
Bool
Int
Figura 3.1: Representao grfica das funes + e todosIguais.
Exemplos de funes:
Uma funo que calcula as razes de uma equao bi-quadrada.
Uma funo que emite o relatrio final com as notas parciais e final dos alunos da disciplina
Tpicos em Linguagem de Programao.
Uma funo que controla a velocidade de um automvel.
Mais exemplos de funes podem ser encontrados em qualquer atividade da vida. Na Figura
3.2 esto mostradas, graficamente, algumas funes baseadas no livro de Simon Thompson [46].
Na Figura, cada desenho do lado esquerdo ou do lado direito um Quadro". Um Quadro o
elemento de entrada da funo que o processa transformando-o em outro Quadro. Por exemplo
a funo espelhaV toma como argumento um Quadro e faz o espelhamento deste Quadro em
relao ao eixo vertical do plano xy.
Neste ponto no ser mostrada uma forma como cada Quadro pode ser implementado, para
ser simulado e processado por um programa. Isto ocorre porque o objetivo aqui apenas mostrar
exemplos de funes. Uma modelagem destes objetos ser mostrada no prximo Captulo.
necessrio salientar a importncia que os tipos dos argumentos e dos resultados tm nas
definies de funes. Eles permitem ao programador estabelecer uma correspondncia bem
definida entre eles e os objetos que modelam, proporcionando uma simulao adequada. Assim,
as funes da Figura reffuncomp tm os tipos:
espelhaV :: Quadro -> Quadro
espelhaH :: Quadro -> Quadro
invertecor :: Quadro -> Quadro
escala :: Quadro -> Quadro
sobrepoe :: Quadro -> Quadro
3.3.1
Construindo funes
Um tipo de dado uma coleo de valores onde todos eles tm as mesmas caractersticas. Por
exemplo, os nmeros inteiros, os caracteres, os strings de caracteres, etc. Os tipos das funes,
59
espelhaV
espelhaH
invertecor
escala
sobrepoe
visto no Captulo 1, pode ser definida em funo das funes mdc (mximo divisor comum) e
div (diviso inteira) entre eles. Vejamos as definies, em Haskell:
mmc :: Int -> Int -> Int
mmc x y = div x*y (mdc x y)
mdc :: Int -> Int -> Int
mdc x y
|x == y
= x
|x > y
= mdc x-y y
|otherwise = mdc y x
div :: Int -> Int -> Int
div x y
|x == y
= 1
|x > y
= 1 + div x-y y
|otherwise = 0
Nas definies mostradas, aparecem alguns elementos novos. Por exemplo, na definio da
funo div, deve ser verificada a semelhana entre a definio matemtica e a definio em Haskell.
Em Haskell, as guardas das definies so colocadas em primeiro lugar, enquanto, na Matemtica,
elas aparecem depois da definio. Outro elemento novo a palavra reservada otherwise que
tem o significado de todas as guardas no referidas anteriormente. Apesar destas diferenas,
pode-se verificar que a traduo das definies matemticas para definies em Haskell um
processo direto, inclusive no que se refere ao uso de definio recursiva. Deve ser observado que a
ordem em que as definies so feitas no tem importncia, desde que estejam no mesmo script.
Algumas linguagens funcionais exigem que os tipos das funes sejam declarados explicitamente pelo programador. Em Haskell, seus projetistas optaram por permitir que os tipos
das funes sejam inferidos pelo sistema. Isto significa que opcional a declarao dos tipos
das funes pelo programador. Mesmo assim, encoraja-se que esta declarao seja feita explicitamente, como uma disciplina de programao. Isto permite ao programador um completo
entendimento do problema e da soluo adotada.
3.3.2
3.3.3
3.4
Haskell, a exemplo de qualquer linguagem de programao, prov uma coleo de tipos primitivos
e tambm permite tipos estruturados, definidos pelo programador, provendo grande flexibilidade
na modelagem de programas.
3.4.1
Os tipos primitivos de Haskell so: o tipo inteiro (Int ou Integer), o tipo booleano (Bool), o
tipo caractere (Char), o tipo cadeia de caracteres (String) e o tipo ponto flutuante (Float ou
Double) e o tipo lista. Nesta seo, vamos analisar cada um destes tipos primitivos, deixando
o tipo lista para ser tratado no Captulo 4, dada a sua importncia nas linguagens funcionais
e, em particular, em Haskell. Ainda neste Captulo, sero estudados o tipo estruturado produto
cartesiano. Os tipos algbricos e os tipos abstratos de dados sero objeto de estudo em captulos
posteriores.
O tipo inteiro (Int ou Integer)
Como em muitas linguagens, o tipo inteiro primitivo em Haskell. Seu domnio de valores
o mesmo das outras linguagens. Os valores do tipo Integer so representados com o dobro
da quantidade de bits necessrios para representar os valores do tipo Int. Seus operadores
aritmticos e relacionais so os mesmos admitidos na maioria das outras linguagens e esto
mostrados na Tabela 3.4.1.
Tabela 3.3: Operadores aritmticos e relacionais dos tipos Int e Integer.
Operador
Tipo
Descrio
+, *
Int -> Int -> Int
adio e multiplicao
Exemplo. Vamos construir uma forma de construo de um programa que envolve operaes
com inteiros. Seja uma empresa de vendas que necessita de respostas para as questes a seguir,
para fundamentar suas decises:
Questo 1: Qual o total de vendas desde a semana 0 at a semana n?
Questo 2: Qual a maior venda semanal entre as semanas 0 e n?
Questo 3: Em que semana ocorreu a maior venda?
Questo 4: Existe alguma semana na qual nada foi vendido?
Questo 5: Em qual semana no houve vendas? (se houve alguma).
Vamos construir algumas funes em Haskell para responder a algumas destas questes,
deixando algumas outras para exerccio do leitor. Para isto necessrio que recordemos as
definies matemticas de funes, vistas no Captulo 1.
63
Questo 1:
Para solucionar a Questo 1, devemos inicialmente construir uma funo venda n que
vai nos informar qual o valor das vendas na semana n. Esta funo pode ser construda de
vrias formas, no entanto ser feita aqui uma definio por casamento de padres, semelhante
definio por enumerao da Matemtica.
venda
venda
venda
venda
Verifiquemos agora como as definies recursivas utilizadas na Matemtica podem ser utilizadas em Haskell. Recordemos a definio matemtica de uma funo usando recurso. Por
exemplo, na definio de uma funo fun, devemos:
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 definio para o nosso caso, vamos construir a funo totaldeVendas
n a partir de venda n, que vai mostrar o total de vendas realizadas at a semana n, inclusive.
totaldeVendas :: Int->Int
totaldeVendas n
|n == 0
= venda 0
|otherwise
= totaldeVendas (n-1) + venda n
Neste caso, em vez de usarmos padres, sero usadas equaes condicionais (booleanas),
tambm chamadas de guardas", cujos resultados so valores True ou False. As guardas so
avaliadas tambm seqencialmente, da mesma forma feita com o casamento de padres. A
palavra reservada otherwise exerce um papel parecido, mas diferente, do _ (sublinhado), j
descrito anteriormente. A clusula otherwise deve ser utilizada apenas quando houver guardas
anteriores, cujos resultados sejam todos False. Isto significa que se a clusula otherwise for
colocada na primeira definio de uma funo, ocorrer um erro, uma vez que no existem
guardas definidas anteriormente. No caso do _ (sublinhado), esta situao no ocasionar erro.
Usando a definio de venda n e de totaldeVendas n, dadas anteriormente, podemos
calcular a aplicao da funo totaldeVendas 2, da seguinte forma:
totaldeVendas 2 =
=
=
=
=
=
=
=
totaldeVendas 1 + venda 2
(totaldeVendas 0 + venda 1) + venda 2
(venda 0 + venda 1) + venda 2
(7 + venda 1) + venda 2
(7 + 2) + venda 2
9 + venda 2
9 + 5
14
totaldeVendas n
|n == 0
= venda 0
|n > 0
= totaldeVendas (n - 1) + venda n
|otherwise = 0
Questo 2:
Vamos agora definir a funo maiorVenda, onde maiorVenda n ser igual a venda n,
se a venda mxima ocorreu na semana n. Se a maior venda ocorreu na semana 0, a funo
maiorVenda 0 ser venda 0. Se a maior venda ocorreu at a semana n, pode estar at a
semana (n-1) ou ser venda n.
maiorVenda :: Int -> Int
maiorVenda n
|n == 0
|maiorVenda (n-1) >= venda n
|otherwise
= venda 0
= maiorVenda (n - 1)
= venda n
Esta mesma funo pode ser construda de outra forma, usando uma funo auxiliar, maximo, que aplicada a dois inteiros retorna o maior entre eles.
maximo :: Int -> Int -> Int
maximo x y
|x >= y
= x
|otherwise = y
maiorVenda n
|n == 0
|otherwise
= venda 0
= maximo (maiorVenda (n - 1)) venda n
= n (equao condicional)
=m
5. Teste as funes que usam vendas com a definio vendas n = n mod 2 + (n + 1) mod
3
6. D uma definio da funo fat que calcula o fatorial de n, onde n um inteiro positivo.
7. D uma definio de uma funo de m e n que retorna o produto
m * (m + 1) * ... *(n 1) * n, para m < n e retorne 0 se m 0.
8. D uma definio de uma funo que retorne i-simo nmero da sequncia de Fibonacci (0,
1, 1, 2...).
O tipo ponto flutuante (Float ou Double)
Os valores do tipo ponto flutuante (nmeros reais) pertencem aos tipos Float ou Double,
da mesma forma que um nmero inteiro pertence aos tipos Int ou Integer. Isto significa que
as nicas diferenas entre valores destes tipos se verificam na quantidade de bits usados para
represent-los. A Tabela 3.4 mostra as principais funes aritmticas pr-definidas na linguagem.
As funes aritmticas recebem a denominao especial de operadores.
Tabela 3.4: Operadores aritmticos de ponto flutuante em Haskell.
+, - *
Float > Float > Float
/
Float > 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 codificao de
programas. Os projetistas de Haskell admitiram a sobrecarga de operadores para ser utilizada na
implementao de uma de suas caractersticas importantes e que permite um grau de abstrao
bem maior que normalmente se encontra em outras linguagens. Esta caracterstica se refere s
classes de tipos (type class), um tema a ser analisado no Captulo 5.
Os tipos Int, Integer, Float e Double so os tipos numricos mais conhecidos em Haskell.
No entanto, outros tipos numricos tambm existem em Haskell. A Tabela 3.5 a seguir apresenta
um resumo dos principais tipos numricos implementados em GHC, onde se verifica que so bem
maiores, em termos de quantidade, que os oferecidos pela grande maioria das outras linguagens
de programao.
66
Descrio
Ponto flutuante de preciso dupla
Ponto flutuante de preciso simples
Inteiro sinalizado de preciso fixa (229 ..229 1)
Inteiro sinalizado de 8 bits
Inteiro sinalizado de 16 bits
Inteiro sinalizado de 32 bits
Inteiro sinalizado de 64 bits
Inteiro sinalizado de preciso arbitrria
Nmeros racionais de preciso arbitrria
Inteiro no sinalizado de preciso fixa
Inteiro no sinalizado de 8 bits
Inteiro no sinalizado de 16 bits
Inteiro no sinalizado de 32 bits
Inteiro no sinalizado de 64 bits
Nome
and
or
inversor
Tipo
&& :: Bool > Bool > Bool
|| :: Bool > Bool > Bool
not :: Bool > Bool
\ - aspas simples
\ - aspas duplas
\34 - ?
Existem algumas funes pr-definidas em Haskell feitas para converter caracteres em nmeros
e vice-versa.
toEnum
:: Int -> Char
fromEnum :: Char -> Int
Exerccio
Defina uma funo mediadasVendas :: Int > Float que d como resultado a mdia
aritmtica entre os valores de vendas 0 at vendas n.
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 patolgico por alguns pesquisadores
de linguagens de programao, os criadores de Haskell admitiram duas formas para este tipo.
Ele um tipo pr-definido como String, mas tambm pode ser considerado como uma lista de
caracteres. Para satisfazer as duas formas, as strings podem ser escritas entre aspas duplas ou
usando a notao de lista de caracteres (entre aspas simples). Por exemplo, "Constantino"tem
o mesmo significado que [C, o, n, s, t, a, n, t, i, n, o], em Haskell.
Todas as funes sobre listas do Prelude.hs podem ser aplicadas a Strings, uma vez que elas
so definidas sobre listas de algum tipo e uma string tambm uma lista.
Toda aplicao de funo, em Haskell, produz um resultado. Podemos verificar isto atravs
da tipificao das funes. Ocorre, no entanto, que para mostrar um resultado, no monitor
ou em outro dispositivo de sada, necessrio definir uma funo para esta tarefa. E qual
deve ser o resultado desta operao? Mostrar um valor no monitor no implica em retorno
de qualquer valor como resultado. Algumas linguagens de programao funcional, como ML,
resolvem este problema de comunicao com o mundo exterior atravs de atribuies 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 comunicao, adota uma semntica de aes baseada em Mnadas, uma teoria bastante
complexa. Uma ao em Haskell um tipo de funo que retorna um valor do tipo IO (),
para ser coerente com o projeto da linguagem. Mostraremos aqui algumas funes usadas na
comunicao com o mundo exterior e formas de aplicao para facilitar o entendimento pelo
leitor. Caso contrrio, seria bastante tedioso usar funes sem poder verificar os resultados de
suas aplicaes.
A primeira destas funes pr-definidas em Haskell putStr :: String > IO () que
utilizada para mostrar strings no monitor. Assim,
putStr "\99a\116" = cat
putStr "Dunga\teh o bicho" = Dunga
eh o bicho
putStr "jeri"++ "coa" ++ "coara" = jericoacoara
Neste caso, as strings podem ser mostradas na tela do monitor. E se um valor no for uma
string? Neste caso, a soluo transformar o valor em uma String e usar a funo putStr. Para
esta misso, foi definida a funo show :: t > String que transforma um valor, de qualquer
68
tipo, em uma string. Alm destas, a funo 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. Defina uma funo para converter letras minsculas em maisculas e que retorne o prprio
caractere se a entrada no for um caractere minsculo.
2. Defina uma funo charParaInt :: Char > Int que converte um dgito em seu valor
(por exemplo, 8 em 8). O valor de um caractere no dgito deve ser 0 (zero).
3. Defina uma funo imprimeDigito :: Char > String que converte um dgito em sua
representao em portugus. Por exemplo, 5 deve retornar cinco".
4. Defina uma funo romanoParaString :: Char > String que converte um algarismo
romano em sua representao em Portugus. Por exemplo, romanoParaString V =
cinco".
5. Defina uma funo emTresLinhas :: String > String > String > String que
toma trs strings e retorna um nico string mostrando os trs strings em linhas separadas.
6. Defina uma funo replica :: String > Int > String que toma um String e um
nmero natural n e retorna n cpias da String, todas juntas. Se n for 0, o resultado deve
ser a String vazia (), se n for 1, retorna a prpria String.
3.4.2
Metodologias de programao
Duas metodologias de construo de programas, bastante difundidas, so a programao topdown e a programao bottom-up, de plena aceitao pelos Engenheiros de Software. Haskell
permite que estas duas tcnicas sejam utilizadas em programas nela codificados. Elas sero
mostradas a seguir.
Programao top-down
Vamos nos referir novamente ao problema das vendas, descrito anteriormente. Suponhamos que
as quantidades de vendas sejam as mostradas na Tabela 3.7.
Tabela 3.7: Quantidade
Semana
0
1
2
Total
Mdia
||\n"
A funo imprimeTotal utilizada apenas para imprimir o total de todas as vendas ocorridas
em todas as semanas. Sua definio a seguinte:
imprimeTotal :: String
imprimeTotal ="||__________________||_\n|| Total |"++show ((vendas 0)+
(vendas 1)+(vendas 2)+(vendas 3))++"
||\n___________________\n"
A funo imprimeMedia deixada como exerccio para o leitor.
A programao top-down tambm conhecida dividir para conquistar", uma estratgia de
guerra utilizada pelos antigos conquistadores. Como tcnica de programao, consiste na diviso de um problema, inicialmente grande, em sub-problemas menores e mais fceis de serem
resolvidos. Esta tcnica envolve trs fases, descritas da seguinte forma:
Fase de diviso. Esta fase consiste na diviso do problema em sub-problemas menores.
Fase de soluo. Esta fase consiste na soluo de cada sub-problema separadamente. Se
o sub-problema ainda for grande, deve ser usada a mesma tcnica de diviso, de forma
recursiva.
Fase de combinao. Esta fase consiste na combinao das solues dos sub-problemas
em uma soluo nica.
Programao bottom-up
A tcnica de programao bottom-up conhecida como programao dinmica e sua motivao
foi resolver um problema potencial que pode surgir ao se utilizar a tcnica de programao
top-down. Este problema diz respeito possibilidade da gerao de um grande nmero de subproblemas idnticos. Se estes sub-problemas forem resolvidos separadamente, pode levar a uma
replicao muito grande de trabalho. A tcnica bottom-up consiste em resolver, inicialmente, as
menores instncias e utilizar estes resultados intermedirios para resolver instncias maiores.
70
Uma representao grfica das chamadas recursivas na execuo desta funo aplicada a 4,
est mostrada na Figura 3.3. Pode ser observado que fib 2 computada duas vezes, fib 1
computada trs vezes e fib 0 computada duas vezes.
fib 4
fib 2
fib 3
fib 2
fib 1
fib 1
fib 1
fib 0
fib 0
fib 1
fib 2
fib 3
fib 4
3.4.3
Haskell tambm admite a possibilidade de que o usurio construa seus prprios tipos de dados, de acordo com as necessidades que ele tenha de simular problemas do mundo real. Os
tipos estruturados so construdos a partir de outros tipos, primitivos ou estruturados. Esta
uma caracterstica muito importante desta linguagem, por facilitar a vida dos programadores,
permitindo um grau muito maior de abstrao dos problemas a serem resolvidos.
O tipo produto cartesiano
O produto cartesiano representado em Haskell pelas tuplas, que podem ser duplas, triplas,
qudruplas, etc. Na maioria das linguagens de programao imperativas, este tipo de dados
implementado atravs 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", "3225-0000", 22)
intP :: (Int, Int)
intP = (35, 45)
As funes sobre tuplas, apesar de poderem ser definidas de vrias formas, so comumente
definidas por pattern matching.
72
Apesar dos resultados das aplicaes das funes somaPar e somaDois serem os mesmos,
elas so distintas. A funo somaPar requer apenas um argumento, neste caso uma tupla,
enquanto a funo somaDois requer dois argumentos do tipo inteiro.
3.4.4
Escopo
O escopo de uma definio a parte de um programa na qual ela visvel e portanto pode ser
usada. Em Haskell, o escopo das definies todo o script, ou seja, todo o arquivo no qual a
definio foi feita. Por exemplo, vejamos a definio de ehImpar n, a seguir, que menciona a
funo ehPar, apesar desta ser definida depois. Isto s 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)
Definies locais
Haskell permite definies locais atravs da palavra reservada where. Por exemplo,
somaQuadrados :: Int -> Int -> Int
somaQuadrados n m = quadN + quadM
where
quadN = n * n
quadM = m * m
73
As definies locais podem incluir outras definies de funes e podem usar definies locais
a uma expresso, usando a palavra reservada let.
let x = 3 + 2; y = 5 - 1 in x2 + 2*x*y - y
As definies locais so visveis apenas na equao onde elas foram declaradas. As variveis
que aparecem do lado esquerdo da igualdade tambm podem ser usadas em definies 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 definies locais podem ser usadas antes delas serem definidas e tambm podem ser usadas
em resultados, em guardas ou em outras definies 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 funo quantosIguaisValor pode ser definida de uma das formas mostradas na Tabela
3.8.
Tabela 3.8: Formas possveis de definio da funo quantosIguaisValor.
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
3.4.5
quantosIguaisValor valor n m p
= ehvalor n + ehvalor m + ehvalor p
where
ehvalor :: Int > Int
ehvalor x = if x == valor then 1 else 0
Expresses condicionais
tiver valor falso, a expresso <exp2> escolhida para ser avaliada e seu resultado ser o da
expresso.
A exemplo de muitas linguagens de programao, Haskell tambm suporta a construo
case, utilizada quando mltiplos valores existem como resultado da avaliao de uma expresso
e, dependendo de cada um deles uma expresso escolhida para ser avaliada. Como exemplo,
uma funo fun que retorna um valor dependendo de sua entrada pode ser definida por:
fun x = case x of
0 -> 50
1 -> 100
2 -> 150
3 -> 200
_ -> 500
3.4.6
Clculos:
A avaliao usada por Haskell chamada de avaliao preguiosa, onde cada expresso avaliada
apenas uma vez e se necessrio. Um clculo s realizado se for realmente necessrio e seu valor
colocado em uma clula da heap. Se ele for novamente solicitado, j est calculado e pronto
para ser utilizado. Por este motivo, Haskell, a exemplo de qualquer linguagem funcional e todas
as modernas linguagens de programao, fazem o gerenciamento dinmico de memria de forma
automtica, conhecido como Garbage Collection ou Coleta de lixo. Para mais detalhes sobre este
tema o leitor deve consultar as referncias [40, 41, 42, 43, 44].
Vamos mostrar, detalhadamente, a seqncia de avaliaes da aplicao de duas funes j
definidas 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
75
3.5
Projeto de programas
2. Defina uma funo que retorne a norma de um vetor dado por suas coordenadas.
3. Se uma linha determinada pelos pontos (x1, y1) e (x2, y2), sua equao definida por
(y - y1)/(x - x1) = (y2 - y1)/(x2 - x1). Defina uma funo do tipo valorY :: Float >
Linha > Float que retorna a ordenada y do ponto (x,y), sendo dados x e uma linha.
4. Dados dois vetores, u e v, determine se eles so, ou no, colineares.
3.5.1
Provas de programas
Uma prova uma argumentao lgica ou matemtica para verificar se alguma premissa ou
no vlida, em quaisquer circunstncias.
Este tema tem importncia fundamental na construo de programas, uma vez que deve-se
buscar a garantia de que o programa esteja correto e que ele realiza apenas a ao para a qual foi
criado. A prova de programas aumenta sua importncia a cada dia, uma vez que os problemas
esto se tornando cada vez mais complexos e desafiadores. Por exemplo, problemas nucleares ou
outros que envolvam vidas humanas podendo colocar em jogo suas sobrevivncias. Para estes
casos, h de existir uma prova de que ele produz a soluo correta.
No entanto, existe um dilema da parte dos usurios que, s vezes, tm dificuldades de realizar
provas de programas, achando ser uma tcnica de fundamentao matemtica e trabalhosa. A
prova de programas em uma linguagem imperativa realmente tediosa, no entanto, como ser
visto, ela bem menos difcil em uma linguagem funcional como Haskell.
Existem, em Haskell, trs maneiras de realizar provas: a prova direta, a prova por casos e a
prova por induo matemtica. Vamos analis-las atravs de exemplos, lembrando ao leitor que
o domnio dessas tcnicas s conseguido atravs da prtica.
Provas diretas
A prova direta feita aplicando as definies das funes. Por exemplo, sejam as funes troca,
cicla e recicla, definidas da seguinte forma:
troca :: (Int, Int) -> (Int, Int)
troca (a, b) = (b, a)
--def 1
= troca (b, a)
= (a, b)
--por def 1
--por def 1
-- (fat 1)
-- (fat 2)
Caso base (P(0)): fatorial 0 = 1 (por fat 1) e 1 > 0. Logo fatorial 0 > 0, significando que
a propriedade vlida para o caso base.
Passo indutivo (P(n)): fatorial n = n * fatorial (n-1), (por fat 2) admitindo-se que n>0. A
hiptese de induo informa que fatorial (n-1) > 0, ou seja, a propriedade P vlida para
n-1. Assim o fatorial de n o produto de dois fatores sendo ambos maiores que zero. O
produto de dois nmeros positivos tambm positivo. Logo, maior que 0.
Concluso: como a propriedade P vlida para o caso base e para o passo indutivo, logo
vlida para todo n natural.
Esta ltima parte, a concluso da prova, um componente importante da prova. comum
ver esquemas de prova, normalmente feitas por iniciantes, onde os dois passos so verificados,
mas no existe a concluso. Neste caso, a prova est incompleta.
Estgio 3:
Estgio 4:
(1)
(2)
(3)
(4)
Estgio 0:
Estgio 1:
Estgio 2:
Estgio 3:
Concluso: como a propriedade vlida para n = 0 e para n > 0, ento vlida para todo
nmero natural n.
Exerccios
1. Prove que, para todo nmero natural n, fatorial (n + 1) power2 n.
2. Prove que, para todo nmero natural n, fib (n+1) power2 (n div 2).
3. D uma prova de que, para todo nmero natural n, venda n 0.
3.6
Resumo
Neste Captulo, foi dado incio ao estudo de programao em Haskell. Foram vistos os tipos de
dados primitivos e as tuplas. Alguns exerccios foram resolvidos para dar uma noo ao usurio
da potencialidade da linguagem e outros foram deixados para o leitor.
O livro de Simon Thompson [46] foi a fonte mais utilizada para este estudo, por ser um livro
que apresenta um contedo terico bem estruturado e fundamentado, alm de muitos exerccios
resolvidos e muitos problemas propostos.
O livro de Richard Bird [4] outra fonte importante de exerccios resolvidos e propostos,
apesar de sua seqncia de abordagens seguir uma ordem distinta, exigindo um conhecimento
anterior sobre programao funcional, o que o torna mais defcil de ser seguido por iniciantes
neste tema.
Outra referncia importante o livro de Paul Hudak [11] que apresenta a programao
funcional em Haskell com exemplos aplicados Multimdia, envolvendo a construo de um
editor grfico e de um sistema utilizado em msica, entre outros. Para quem imagina que
Haskell s aplicado a problemas da Matemtica, esta referncia pe por terra este argumento.
81
82
Captulo 4
O tipo Lista
Commputers specially designed for applicative languages
implement recursive functions very efficiently.
Also, architectural features can be included in conventional
computers that significantly increase
the speed of recursive-function invocations.
(Bruce J. MacLennan in [25])
4.1
Introdio
Lista 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 funes para a
sua manipulao. A notao utilizada para as listas colocar seus elementos entre colchetes.
Por exemplo, [1,2,3,4,1,3] uma lista de inteiros, [True,False] uma lista de booleanos, [a,
a, b] uma lista de caracteres e ["Marta", "Marlene"] uma lista de strings. H, no
entanto, que se diferenciar as listas homogneas, que so as listas onde todos os valores so do
mesmo tipo, das listas heterogneas, onde os componentes podem ter mais de um tipo. Haskell
s admite listas homogneas. Por exemplo, [False, 2,"Maria"] no uma lista em Haskell, por
ser heterognea. Em compensao, podemos ter a lista [totalVendas, totalVendas], que tem
o tipo [Int > Int] como tambm a lista [[12,1], [3,4], [4,4,4,4,4], [ ]] que tem o tipo [[Int]],
uma lista de listas de inteiros, alm 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, [ ] do tipo [Int] ou [Bool] ou
[Int > Int], significando que a lista vazia est na interseo de todas as listas, sendo o
nico elemento deste conjunto.
a lista no 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 chamado de cabea e x a cauda da lista.
Algumas caractersticas importantes das listas em Haskell, so:
A ordem em uma lista importante, ou seja, [1,3] /= [3,1] e [False] /= [False, False].
A lista [m .. n] igual lista [m, m+1, ..., n]. Por exemplo, [1 .. 5] = [1, 2, 3, 4, 5].
A lista [3.1 .. 7.0] = [3.1, 4.1, 5.1, 6.1].
83
A lista [m,p .. n] igual lista de m at n em passos de p-m. 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].
A lista [m..n], para m>n, vazia. Por exemplo, [7 .. 3] = [ ].
A lista vazia no tem cabea e nem cauda. Se tivesse qualquer destes dois componentes,
no seria vazia.
A lista no vazia tem cabea e cauda, onde a cauda tambm uma lista, que pode ser
vazia, ou no.
4.2
significando que cons (:) associa seus componentes pela direita, ou seja: a : b : c = a : (b : c)/ =
(a : b) : c
Mas qual o tipo de cons? Observando que 4 : [3] = [4, 3], ento cons tem o tipo:
(:) :: Int -> [Int] -> [Int]
No entanto, verificamos tambm que True : [False] = [True, False]. Agora cons tem o
tipo:
(:) :: Bool -> [Bool] -> [Bool]
Isto mostra que o operador cons polimrfico. Desta forma, seu tipo :
(:) :: t -> [t] -> [t]
onde [t] uma lista de valores, de qualquer tipo, desde que seja homognea.
Construindo funes sobre listas
Em Haskell, j existe um grande nmero de funes pr-definidas para a manipulao de listas.
Estas funes fazem parte do arquivo Prelude.hs, carregado no momento em que o sistema
chamado e permanece ativo at o final da execuo. Um resumo destas funes, com seus tipos
e exemplos de utilizao, pode ser visto na Tabela 4.1.
Tabela 4.1: Algumas funes polimrficas do Prelude.hs.
Funo
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]!!0=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
85
Alm das funes pr-definidas, o usurio tambm pode construir funes para manipular
listas. Aqui a criatividade o limite. Vamos mostrar isto atravs de um exemplo simples e depois
atravs de um exemplo mais complexo.
Vamos construir uma funo que verifica se um determinado elemento pertence, ou no, a
uma lista. Para isto vamos construir a funo pertence:
pertence :: Int -> [Int] -> Bool
pertence b [ ]
= False
pertence b (a:x) = (b == a) || pertence b x
Esta mesma funo tambm pode ser codificada de outra forma, usando guardas:
pertence b [ ] = False
pertence b (a:x)
|b == a
= True
|otherwise = pertence x b
Modelagem. Voltemos nossa ateno agora para os exemplos das funes espelhaH e espelhaV, vistas no Captulo anterior. Suponhamos que um cavalo seja um objeto do tipo Quadro.
Um Quadro pode ser modelado por uma matriz (12x12), de caracteres. Desta forma, um cavalo
uma lista de 12 linhas e cada linha uma lista de 12 caracteres, ou seja, um cavalo uma lista
de listas de caracteres, conforme pode ser visto graficamente na representao a seguir, onde os
pontos esto colocados apenas para facilitar a contagem, ou seja, eles esto colocados apenas
para representar os caracteres em branco. Os resultados das aplicaes das funes espelhaH e
espelhaV a um cavalo tambm esto mostrados na mesma Figura, a seguir.
cavalo
.......##...
.....##..#..
...##.....#.
..#.......#.
..#...#...#.
..#...###.#.
..#...#..##.
..#...#.....
...#...#....
....#..#....
.....#.#....
......##....
espelhaH cavalo
......##....
.....#.#....
....#..#....
...#...#....
..#...#.....
..#...#..##.
..#...###.#.
..#...#...#.
..#.......#.
...##.....#.
.....##..#..
.......##...
espelhaV cavalo
...##.......
..#..##.....
.#.....##...
.#.......#..
.#...#...#..
.#.###...#..
.##..#...#..
.....#...#..
....#...#...
....#..#....
....#.#.....
....##......
86
A funo reverse, quando aplicada a uma lista de elementos de qualquer tipo produz, 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 significa
que a funo reverse aplicada a cada linha do cavalo, que uma lista de caracteres, d, como
resultado, a mesma linha mas com a ordem de seus caracteres invertida. A funo map toma
a funo reverse, o primeiro de seus dois argumentos, e a aplica a cada uma das linhas de seu
segundo argumento (um cavalo). As funes map e reverse so pr-definidas em Haskell e elas
sero objeto de estudo mais profundo nas prximas sees.
Algumas concluses importantes podem ser tiradas a partir destes exemplos:
a funo reverse pode ser aplicada a uma lista de valores de qualquer tipo. Isto significa
que uma mesma funo pode ser aplicada a mais de um tipo de dados. Isto significa
polimorfismo, ou seja, a utilizao genrica de uma funo, aumentando a produtividade
de software.
Um argumento da funo map reverse, uma funo. Isto significa que uma funo pode
ser passada como parmetro para uma outra funo. Neste caso, diz-se que as funes so
consideradas como cidados de primeira categoria, permitindo-se a elas os mesmos direitos
que qualquer outro tipo de dado.
O resultado da aplicao da funo map funo reverse uma outra funo que
aplicada a um outro argumento que, neste caso, uma lista. Neste caso, diz-se que, nas
linguagens funcionais, as funes so de alta ordem, ou seja, podem ser passadas como
argumentos de uma funo e tambm podem retornar como resultados da aplicao de
uma funo.
Exerccios
1. Dada a definio da funo dobra
dobra :: [Int] -> [Int]
dobra [ ] = [ ]
dobra (a:x) = (2 * a) : dobra x
Calcule dobra [3,4,5] passo a passo.
2. Escreva [False, False, True] e [2] usando : e [ ].
3. Calcule somaLista [30, 2, 1, 0], dobra [0] e cafe++com++ leite.
4. Defina a funo product :: [Int] > Int que retorna o produto de uma lista de inteiros.
5. Defina a funo and :: [Bool] > Bool que retorna a conjuno da lista. Por exemplo,
and [e1 , e2 , . . . , en ] = e1 &&e2 && . . . &&en .
6. Defina a funo concat :: [[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 ordenao de uma lista de
inteiros.
Uma forma de ordenar uma lista no vazia inserir a cabea da lista no local correto, que
pode ser na cabea da lista ou pode ser na cauda j ordenada. Por exemplo, para ordenar a
87
lista de inteiros [3,4,1], devemos inserir 3 na cauda da lista, j ordenada, ou seja em [1,4]. Para
que esta cauda j esteje ordenada necessrio apenas chamar a mesma funo de ordenao,
recursivamente, para ela. Vamos definir uma funo de ordenao, ordena, que utiliza uma
funo auxiliar insere, cuja tarefa inserir cada elemento da lista no lugar correto.
ordena :: [Int] -> [Int]
ordena [ ] = [ ]
ordena (a:x) = insere a (ordena x)
A lista vazia considerada ordenada por vacuidade. Por isto a primeira definio. Para o
caso da lista no vazia, devemos inserir a cabea (a) na cauda j ordenada (ordena x). Falta
apenas definir a funo insere, que 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 mtodo de ordenao conhecido como insero direta e o leitor deve observar a simplicidade como ele implementado em Haskell. Sugerimos comparar esta implementao com
outra, em qualquer linguagem convencional. Alm disso, tambm se deve considerar a possibilidade de aplicao desta mesma definio listas de vrios tipos de dados, significando que a
definio pode ser polimrfica. Isto significa que, na definio da funo insere, a nica operao
exigida sobre os valores dos elementos da lista a ser ordenada que eles possam ser comparados
atravs da operao . Uma lista de valores, de qualquer tipo de dados, onde esta operao seja
possvel entre estes valores, pode ser ordenada usando esta definio.
Exerccios
1. Mostre todos os passos realizados na chamada ordena [2, 8, 1].
2. Defina uma funo numOcorre :: [t] > t > Int, onde numOcorre l s retorna o
nmero de vezes que o tem s aparece na lista l.
3. D uma definio da funo pertence, vista anteriormente, usando a funo numOcorre,
do tem anterior.
4. Defina a funo ocorreUmaVez :: [Int] > [Int] que retorna a lista de nmeros que
ocorrem exatamente uma vez em uma lista. Por exemplo, ocorreUmaVez [2,4,2,1,4] =
[1].
4.3
O casamento de padres j foi analisado anteriormente, mas sem qualquer profundidade e formalismo. Agora ele ser visto com uma nova roupagem, apesar de usar conceitos j conhecidos.
Os padres em Haskell so dados por:
valores literais como -2, C e True.
variveis como x, num e maria.
o caractere _ (sublinhado) casa com qualquer argumento.
88
um padro de tuplas (p1 , p2 , ..., pk ). Um argumento para casar com este padro tem de ser
da forma (v1 , v2 , ..., vk ), onde cada vi deve ser do tipo pi .
um construtor aplicado a outros padres. Por exemplo, o construtor de listas (p1 : p2 ),
onde p1 e p2 so padres
Nas linguagens funcionais, a forma usada para verificar se um padro casa, ou no, com um
valor realizada da seguinte maneira:
primeiramente, verifica-se se o argumento est na forma correta e
depois associam-se valores s variveis dos padres.
Exemplo. A lista [2,3,4] casa com o padro (a : x) porque:
1. ela tem cabea e tem cauda, portanto uma lista correta. Portanto,
2. 2 associado com a cabea a e [3,4] associada com a cauda x.
Pode-se perguntar: em que situaes um argumento a casa com um padro p? Esta questo
respondida atravs da seguinte lista de clusulas:
se p for uma constante, a casa com p se a == p.
se p for uma varivel, a casa com p e a ser associado com p.
se p for uma tupla de padres (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 padres (p1 : p2 ), a casa com p se a for uma lista no vazia. Neste
caso, a cabea de a associada com p1 e a cauda de a associada com p2 .
se p for um sublinhado (_), a casa com p, mas nenhuma associao feita. O sublinhado
age como se fosse um teste.
Exemplo. Seja a funo zip definida da seguinte forma:
zip (a:x) (b:y) = (a, b) : zip x y
zip _ _ = [ ]
Se os argumentos de zip forem duas listas, ambas no vazias, forma-se a tupla com as cabeas
das duas listas, que ser incorporada lista de tuplas resultante e o processo continua com a
aplicao recursiva de zip s caudas das listas argumentos. Este processo continua at que o
padro de duas listas no vazias falhe. No momento em que uma das listas, ou ambas, for vazia,
o resultado ser a lista vazia e a execuo da funo termina. Vamos verificar o resultado de
algumas aplicaes.
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. Defina uma funo somaTriplas que soma os elementos de uma lista de triplas de nmeros,
(c, d, e).
89
2. Defina uma funo somaPar que d como resultado a lista das somas dos elementos de
uma lista de pares de nmeros.
3. Calcule somaPar [(2,3), (96, -7)], passo a passo.
4. Defina uma funo unzip :: [(Int, Int)] > ([Int], [Int]) que transforma uma lista de
pares em um par de listas. Sugesto: defina antes as funes 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].
Exemplo: Agora vamos analisar um exemplo mais detalhado da aplicao de listas em Haskell,
baseado em Simon Thompson [46]. Seja um banco de dados definido para contabilizar as retiradas
de livros de uma Biblioteca, por vrias pessoas. Para simular esta situao, 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 fictcia 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 definir funes para realizar as seguintes tarefas:
1. Operaes de consulta:
Uma funo que informa os livros que uma determinada pessoa tomou emprestado.
Uma funo que informa todas as pessoas que tomaram emprestado um determinado
livro.
Uma funo que informa se um determinado livro est ou no emprestado.
Uma funo que informa a quantidade de livros que uma determinada pessoa tomou
emprestado.
2. Operaes de atualizao:
Uma funo que atualiza o banco, quando um livro emprestado a algum.
Uma funo que atualiza o banco quando um livro devolvido.
Inicialmente, vamos construir a funo livrosEmprestados que pode ser utilizada para
servir de roteiro para a definio das outras funes 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
90
4.4
No exemplo 2) deve-se notar a importncia que tem ordem em que os geradores so colocados.
Se o gerador de b viesse antes do gerador de a, ocorreria um erro.
No exemplo 3) tambm deve ser observada a ordem em que os geradores foram colocados
para que seja possvel a gerao correta dos tringulos retngulos.
A funo livrosEmprestados, definida na seo 4.2, pode ser re-definida usando expresses
ZF, da seguinte forma:
livrosEmprestados :: BancodeDados -> Pessoa -> [Livro]
livrosEmprestados db fulano = [liv | (pes, liv) <- db, pes == fulano]
Exerccios:
1. Re-implemente as funes de atualizao do Banco de Dados para a Biblioteca, feitas na
seo 4.2, usando compreenso de listas, em vez de recurso explcita.
2. Como a funo membro :: [Int] > Int > Bool pode ser definida usando compreenso
de listas e um teste de igualdade?
Exemplo. Vamos agora mostrar um exemplo mais completo, baseado na referncia [46], que
mostra algumas das possibilidades que as compreenses oferecem. Seja um processador de texto
simples que organiza um texto, identando-o pela esquerda. Por exemplo, o texto
Maria
gostava de bananas e
estava apaixonada
por
Joaquim e
tomou
veneno para
morrer.
deve ser transformado em um texto mais organizado, ficando da seguinte forma:
Maria gostava de bananas e estava apaixonada
por Joaquim e tomou veneno para morrer.
Para isto, vamos construir algumas funes para realizar tarefas auxiliares. Inicialmente,
devemos observar que uma palavra uma sequncia de caracteres que no tem espaos em
branco dentro dela. Os espaos em branco so definidos da seguinte forma:
espacoEmBranco :: [Char]
espacoEmBranco = [\n, \t, ]
Vamos definir a funo pegaPalavra que, quando aplicada a uma string, retira a primeira
palavra desta string se a string no iniciar com um espao em branco. Assim pegaPalavra
"bicho besta"= "bicho" e pegaPalavra " bicho"= [ ] porque a string iniciada com um
caractere em branco.
Uma definio para ela pode ser feita, usando a funo pertence, definida na seo 4.1, que
verifica se um determinado caractere a pertence, ou no, a uma string:
pegaPalavra :: String -> String
pegaPalavra [ ] = [ ]
pegaPalavra (a:x)
|pertence a espacoEmBranco
= [ ]
|otherwise
= a : pegaPalavra x
94
J a funo tiraPalavra, quando aplicada a uma string, retira a primeira palavra da string
e retorna a string restante, tendo como seu primeiro caractere o espao em branco. Assim,
tiraPalavra "bicho feio"= " feio".
tiraPalavra :: String -> String
tiraPalavra [ ] = [ ]
tiraPalavra (a : x)
|pertence a espacoEmBranco = (a : x)
|otherwise
= tiraPalavra x
Est claro que necessrio construir uma funo para retirar os espaos em branco da frente
das palavras. Esta funo ser tiraEspacos que, aplicada a uma string iniciada com um ou
mais espaos em branco, retorna outra string sem estes espaos em branco.
tiraEspacos :: String -> String
tiraEspacos [ ] = [ ]
tiraEspacos (a : x)
|pertence a espacoEmBranco = tiraEspacos x
|otherwise
= (a : x)
Resta agora formalizar como uma string, st, deve ser transformada em uma lista de palavras,
assumindo que st no seja iniciada por um espao em branco. Assim,
a primeira palavra desta lista de palavras ser dada por pegaPalavra st,
o restante da lista ser construdo dividindo-se a string que resulta da remoo da primeira
palavra e dos espaos em branco que a seguem, ou seja, a nova transformao ser feita
sobre tiraEspacos (tiraPalavra st).
type Palavra = String
divideEmPalavras :: String -> [Palavra]
divideEmPalavras st = divide (tiraEspacos st)
divide :: String -> [Palavra]
divide [ ] = [ ]
divide st = (pegaPalavra st) : divide (tiraEspacos (tiraPalavra st))
Vamos acompanhar a seqncia de operaes da aplicao divideEmPalavras "bicho
bom".
divideEmPalavras " bicho bom"
= divide (tiraEspacos " bicho bom")
= divide "bicho bom"
= (pegaPalavra "bicho bom") : divide (tiraEspacos (tiraPalavra "bicho bom"))
= "bicho": divide (tiraEspaco " bom")
= "bicho" : divide "bom"
= "bicho" : (pegaPalavra "bom") : divide (tiraEspacos (tiraPalavra "bom"))
= "bicho" : "bom" : divide (tiraEspacos [ ])
= "bicho" : "bom" : divide [ ]
= "bicho" : "bom" : [ ]
= ["bicho", "bom"]
95
Deve ser observado que j existe a funo words, definida no Prelude, que produz o mesmo
resultado que a funo divideEmPalavras. Esta definio foi aqui mostrada para fins de
entendimento do usurio. Agora necessrio tomar uma lista de palavras e transform-la em
uma lista de linhas, onde cada linha uma lista de palavras, com um tamanho mximo (a linha).
Para isto, vamos definir uma funo que forme uma nica linha com um tamanho determinado.
type Linha = [Palavra]
formaLinha :: Int -> [Palavra] -> Linha
Inicialmente, vamos admitir algumas premissas:
Se a lista de palavras for vazia, a linha tambm ser vazia.
Se a primeira palavra disponvel for p, ela far parte da linha se existir vaga para ela na
linha. O tamanho de p, length p, ter que ser menor ou igual ao tamanho da linha (tam).
O restante da linha construdo a partir das palavras que restam, considerando agora uma
linha de tamanho tam-(length p + 1).
Se a primeira palavra no se ajustar, a linha 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 seqncia de aplicao da funo:
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 funo, tiraLinha, que receba como parmetros um tamanho que uma
linha deve ter e uma lista de palavras e retorne esta lista de palavras sem a primeira linha. Esta
funo ser deixada como exerccio, no entanto, indicamos seu tipo.
tiraLinha :: Int -> [Palavra] -> [Palavra]
Agora necessrio juntar as coisas. Vamos construir uma funo 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 funo recursivamente lista restante.
divideLinhas :: Int -> [Palavra] -> [Linha]
divideLinhas _ [ ]
= [ ]
divideLinhas tamLin x =
formaLinha tamLin x : divideLinhas (tiraLinha tamLin x)
96
Falta agora construir uma funo que transforme uma string em uma lista de linhas, formando
o novo texto identado esquerda.
preenche :: Int -> String -> [Linha]
preenche tam st = divideLinhas tam (divideEmPalavras st)
Finalmente deve-se juntar as linhas para que se tenha o novo texto, agora formando uma
string identada esquerda. Ser mostrada apenas o seu tipo, deixando sua definio como
exerccio.
juntaLinhas :: [Linha] -> String
Exerccios
1. D uma definio de uma funo juntaLinha :: Linha > String que transforma uma
linha em uma forma imprimvel. Por exemplo, juntaLinha ["bicho", "bom"] = "bicho
bom".
2. Use a funo juntaLinha do exerccio anterior para definir uma funo juntaLinhas ::
[Linha] > String que junta linhas separadas por \n.
3. Modifique a funo juntaLinha de forma que ela ajuste a linha ao tamanho tam, adicionando uma quantidade de espaos entre as palavras.
4. Defina uma funo estat :: String > (Int, Int, Int) que aplicada a um texto retorna
o nmero de caracteres, palavras e linhas do texto. O final de uma linha sinalizado pelo
caractere newline (\n). Defina tambm uma funo novoestat :: String > (Int, Int,
Int) que faz a mesma estatstica sobre o texto, aps ser ajustado.
5. Defina uma funo subst :: String > String > String > String de forma que
subst velhaSub novaSub st faa a substituio 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 no ocorrer em st, o resultado deve ser st).
4.5
Provavelmente, a maioria dos leitores j estejam familiarizados com a idia de listas de listas,
de dar nomes s listas ou de funes que adimitem listas como seus parmetros. O que talvez
parea extranho para muitos a idia de listas de funes ou de funes que retornam outras
funes com resultados. Esta uma caracterstica importante das linguagens funcionais, A idia
central a de que as funes so consideradas com os mesmos direitos que qualquer outro tipo
de dado, dizendo-se, corriqueiramente, que elas so cidads de primeira categoria.
Vamos imaginar uma funo twice que, quando aplicada a uma outra funo, por exemplo,
f, produza, como resultado, uma outra funo que aplicada a seu argumento tenha o mesmo
efeito da aplicao da funo f, duas vezes. Assim,
twice f x = f (f x)
Como outro exemplo, vamos considerar a seguinte definio em Haskell:
let suc
= soma 1
soma x = somax
where somax y = x + y
in suc 3
97
O resultado desta aplicao 4. O efeito de soma x criar uma funo chamada somax,
que adiciona x a algum nmero natural, no caso, y. Desta forma, suc uma funo que adiciona
o nmero 1 a um nmero natural qualquer.
A partir destes exemplos, podemos caracterizar duas ferramentas importantes, a saber:
1. Um novo mecanismo para passar dois ou mais parmetros para uma funo. Apesar da
funo soma ter sido declarada com apenas um parmetro, poderamos cham-la, por
exemplo, como soma 3 4, que daria como resultado 7. Isto significa que soma 3 tem
como resultado uma outra funo que, aplicada a 4, d como resultado o valor 7.
2. Um mecanismo de aplicao parcial de funes. Isto quer dizer que, se uma funo for
declarada com n parmetros, podemos aplic-la a m destes parmetros, mesmo que m seja
menor que n.
As funes de alta ordem so usadas de forma intensa em programas funcionais, permitindo
que computaes complexas sejam expressas de forma simples. Vejamos algumas destas funes,
muito utilizadas na prtica da programao funcional.
4.5.1
A funo map
Um padro de computao que explorado como funo de alta ordem envolve a criao de uma
lista (listaNova) a partir de uma outra lista (listaVelha), onde cada elemento de listaNova
tem o seu valor determinado atravs da aplicao de uma funo 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 transformado em uma tupla, onde o primeiro elemento seja o
prprio nome e o segundo seja a quantidade de caracteres do nome. Para isso, ser construda
uma funo listaTupla de forma que
listaTupla ["Dunga","Constantino"] = [("Dunga",5),("Constantino",11)]
Antes vamos criar a funo 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 s = (s, length s)
Agora a funo listaTupla pode ser definida da seguinte maneira:
listaTupla :: [String] -> [(String, Int)]
listaTupla [ ]
=
[ ]
listaTupla (s : xs) = (tuplaNum s) : listaTupla xs
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]
Como na definio da funo anterior, vamos construir a funo auxiliar, dobra, da seguinte
forma:
98
=
=
=
=
=
=
=
4.5.2
Funes annimas
J foi visto, e de forma enftica, que, nas linguagens funcionais, as funes podem ser usadas
como parmetros para outras funes. No entanto, seria um desperdcio definir uma funo que
s pudesse ser utilizada como parmetro para outra funo, ou seja, a funo s fosse utilizada
neste caso e em nenhum outro mais. No caso da seo anterior, as funes dobra e tuplaNum,
possivelmente, s sejam utilizadas como argumentos das funes dobraLista e listaTupla.
Uma forma de declarar funes para serem utilizadas apenas localmente usar a clusula
where. Por exemplo, dado um inteiro n, vamos definir uma funo que retorne uma outra funo
de inteiro para inteiro que adiciona n a seu argumento.
100
fx
g
g (f x) (f y)
fy
=
=
=
=
=
=
=
No entanto, existe um pequeno problema na definio de fold. Se ela for aplicada a uma
funo e uma lista vazia ocorrer um erro. Para resolver este impasse, foi pr-definida, em
Haskell, uma outra funo para substituir fold, onde este tipo de erro seja resolvido. Esta a
funo foldr, definida 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 verificar como algumas funes so definidas usando foldr.
Exemplos:
concat :: [[t]] -> [t]
concat xs = foldr (++) [ ] xs
A funo filter
A funo filter uma outra funo de alta ordem, pr-definida em todas as linguagens funcionais,
de bastante utilizao. Resumidamente, ela escolhe dentre os elementos de uma lista, aqueles
que tm uma determinada propriedade. Vejamos alguns exemplos:
1. filter ehPar [2,3,4] = [2,4].
2. Um nmero natural perfeito se a soma de seus divisores, incluindo o nmero 1, for o
prprio nmero. Por exemplo, 6 o primeiro nmero natural perfeito, porque 6 = 1+2+3.
Vamos definir uma funo que mostra os nmeros 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 nmeros perfeitos?
3. A lista de todos os nmeros 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 Maro 1958"= "181958"
A definio formal de filter :
filter :: (t -> Bool) -> [t] -> [t]
filter p [ ] = [ ]
filter p (a : x)
ou
|p a
= a : filter p x
|otherwise = filter p x
4.6
filter p l = [a | a <- l, p a]
Polimorfismo
Uma caracterstica muito importante das linguagens funcionais que suas definies podem ser
polimrficas, um mecanismo que aumenta o poder de expressividade de qualquer linguagem.
Polimorfismo uma das caractersticas responsveis pela alta produtividade de software, proporcionada pelo aumento da reusabilidade.
Polimorfismo a capacidade de aplicar uma mesma funo a vrios tipos de dados, representados por um tipo varivel. No deve ser confundido com sobrecarga, denominada por
muitos pesquisadores como polimorfismo ad hoc, que consiste na aplicao de vrias funes com
o mesmo nome a vrios tipos de dados. Haskell permite os dois tipos de polimorfismo, sendo que
a sobrecarga feita atravs de um mecanismo engenhoso chamado de type class, um tema a
ser estudado no prximo Captulo.
A funo length pr-definida em Haskell, da seguinte maneira:
103
4.6.1
Tipos variveis
Quando uma funo tem um tipo envolvendo um ou mais tipos variveis, diz-se que ela tem
um tipo polimrfico. Por exemplo, j vimos anteriormente que a lista vazia um elemento de
qualquer tipo de lista, ou seja, [ ] est na interseo dos tipos [Int], [Bool] ou [Char], etc. Para
explicitar esta caracterstica denota-se que [ ] :: [t], sendo t uma varivel que pode assumir
qualquer tipo. Assim, cons tem o tipo polimrfico (:) :: t > [t] > [t].
As seguintes funes, algumas j definidas anteriormente, tm os tipos:
length :: [t] -> Int
(++) :: [t] -> [t] -> [t]
rev :: [t] -> [t]
id :: t -> t
zip :: [t] -> [u] -> [(t, u)]
4.6.2
Alguma dificuldade pode surgir nas definies dos tipos das funes quando elas envolvem tipos
variveis, uma vez que podem existir muitas instncias de um tipo varivel. Para resolver este
dilema, necessrio que o tipo da funo seja o tipo mais geral possvel, que definido da seguinte
forma:
Definio. Um tipo w de uma funo f o tipo mais geral de f se todos os tipos de f forem
instncias de w.
A partir desta definio pode-se observar que o tipo [t] > [t] > [(t, t)] uma instncia
do tipo da funo zip, mas no o tipo mais geral, porque [Int] > [Bool] > [(Int, Bool)]
um tipo para zip, mas no uma instncia de [t] >[t] >[(t,t)].
Exemplos de algumas funes polimrficas.
rep :: Int -> t -> [t]
rep 0 ch = [ ]
rep n ch = ch : rep (n - 1) ch
fst :: (t, u) -> t
fst (x, _) = x
snd (_, y) = y
4.7
Induo estrutural
Estgio 3:
Estgio 4:
Vejamos agora, dois exemplos completos de esquemas de provas que envolvem todos os estgios enumerados anteriormente.
Exemplo 1. Dadas as definies a seguir:
somaLista [ ]
= 0
somaLista (a : x) = a + somaLista x
(1)
(2)
dobra [ ]
= [ ]
dobra (a : x) = (2 * a) : dobra x
(3)
(4)
(5)
Estgio 2:
2 * somaLista [ ] = sumList (dobra [ ])
(6)
(7)
(8)
por (1)
pela aritmtica
por (2).
por (4)
por (2)
pela hiptese de induo
pela distributividade de *.
106
por (3)
por (1)
(2)
x ++ (y ++ z) = (x ++ y) ++ z
Estgio 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)
Estgio 3:
caso base: lado esquerdo
[ ] ++ (y ++ z)
= y ++ z
por (1)
por (1)
Como o lado esquerdo e o lado direito do caso base so iguais, ento a propriedade vlida
para ele.
Estgio 4:
passo indutivo: lado esquerdo
(a : x) ++ (y ++ z)
= a : (x ++ (y ++ z))
por (2)
por (2)
por (2)
pela hiptese de induo.
Como o lado esquerdo e o lado direito do passo indutivo so iguais, ento a propriedade
vlida para ele.
Concluso: como a assertiva vlida para o caso base e para o passo indutivo, ento ela
verdadeira para todas as listas finitas.
Exerccios
1. Prove que, para todas as listas finitas x, x ++ [ ] = x.
2. Tente provar que x ++ (y ++ z) = (x ++ y) ++ z usando induo estrutural sobre
z. H alguma coisa esquisita com esta prova? O qu?
107
4.8
Composio de funes
Uma forma simples de estruturar um programa constru-lo em etapas, uma aps outra, onde
cada uma delas pode ser definida separadamente. Em programao funcional, isto feito atravs
da composio de funes, uma propriedade matemtica s implementada nestas linguagens e
que aumenta, enormemente, a expressividade do programador.
A expresso f(g(x)), normalmente, escrita pelos matemticos como (f.g)(x), onde o .
(ponto) o operador de composio de funes. Esta notao importante porque separa a
parte das funes da parte dos argumentos. O operador de composio um tipo de funo,
cujos argumentos so duas funes e o resultado tambm uma funo.
Para facilitar nosso entendimento, vamos retornar funo divideEmPalavras, definida na
seo 4.3 da seguinte forma:
divideEmPalavras :: String -> [Palavra]
divideEmPalavras st = divide (tiraEspacos st)
Esta funo pode ser reescrita como: divideEmPalavras st = divide . tiraEspacos st
Como mais um exemplo, vamos considerar a definio
let quad
quadrado x
sucessor x
compoe (f,g)
in quad 3
=
=
=
=
A resposta a esta aplicao 16. O interesse maior aqui est na definio da funo compoe.
Ela toma um par de parmetros, f e g (ambos funes) e retorna uma outra funo, h, cujo
efeito de sua aplicao a composio das funes f e g. Esta forma de codificao torna a
composio explcita sem a necessidade de aplicar cada lado da igualdade a um argumento.
Como sabido, nem todo par de funes pode ser composto. O tipo da sada da funo g
tem de ser o mesmo tipo da entrada da funo f. O tipo de . : ( . ) :: (u > v) > (t
> u) > (t > v).
A composio associativa, ou seja, f . (g . h) = (f . g) . h, que deve ser interpretado
como faa h, depois faa g e finalmente faa f ".
108
4.8.1
Composio avanada
A ordem em f . g importante (faa g e depois f ). Podemos fazer a composio ter o sentido das
aes propostas, usando a composio avanada. Deve ser lembrado que esta forma apenas para
fins de apresentao, uma vez que a definio, como ser vista, feita em funo da composio e
nada aumenta em expressividade. A composio avanada ser denotada por > . > indicando a
sequencializao da aplicao das funes, uma vez que, na composio, ela realizada na ordem
inversa de aparncia no texto. A definio de > . > feita da seguinte forma:
infixl 9 >.>
(>.>) :: (t -> u) -> (u -> v) -> (t -> v)
g >.> f = f . g
A funo divideEmPalavras, definida anteriormente, pode ser re-definida usando composio avanada, da seguinte forma:
divideEmPalavaras = tiraEspacos > . > divide
Deve-se ter o cuidado de observar que f . g x diferente de (f . g) x porque a aplicao
de funes tem prioridade sobre a composio. Por exemplo, succ . succ 1 resultar em erro
porque succ 1 ser realizada primeiro e retornar um inteiro (2), fazendo com que a composio
do primeiro succ seja feita com um nmero inteiro e no com uma funo. Neste caso, os
parnteses devem ser utilizados para resolver ambigidades.
4.8.2
Pode-se generalizar twice, indicando um parmetro que informe quantas vezes a funo deve
ser composta com ela prpria:
iter :: Int -> (t -> t) -> (t -> t)
iter 0 f = id
iter n f = f >.> iter (n - 1) f
Por exemplo, podemos definir 2n como iter n duplica e vamos mostrar a seqncia de
operaes para n=2.
iter 2 duplica
=
=
=
=
=
Nesta definio, usamos o fato de que f . id = f. Estamos tratando de uma nova espcie de
igualdade, que a igualdade de duas funes. Mas como isto pode ser feito? Para isto, devemos
109
Isto significa que para um argumento x, qualquer, as duas funes se comportam exatamente
da mesma forma.
Vamos fazer uma pequena discusso sobre o que significa a igualdade entre duas funes.
Existem dois princpios que devem ser observados quando nos referimos igualdade de funes.
So eles:
Princpio da extensionalidade: Duas funes, f e g, so iguais se elas produzirem
exatamente os mesmos resultados para os mesmos argumentos".
Princpio da intencionalidade: Duas funes, f e g so iguais se tiverem as mesmas
definies".
Se estivermos interessados nos resultados de nossos programas, tudo o que nos interessa so
os valores dados pelas funes e no como estes valores so encontrados. Em Haskell, devemos
usar extensionalidade quando estivermos interessados no comportamento das funes. Se estivermos interessados na eficincia ou outros aspectos de desempenho de programas devemos usar a
intencionalidade.
Exerccios
1. Mostre que a composio de funes associativa, ou seja, f, g e h, f . (g . h) = (f .
g) . h
2. Prove que n Z + , iter n id = id.
3. Duas funes f e g so inversas se f . g = id e g . f = id. Prove que as funes curry e
uncurry, definidas a seguir, so 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 definio de map e da composio de funes 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 [ ]
=[ ]
110
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 g x))
=f (g a) : (map f ( map g) x)
=f (g a) : (map f . map g) x
(3)
(2)
(3)
Concluso. Como a propriedade vlida para a lista vazia e para a lista no vazia, ento ela
vlida para qualquer lista homognea finita.
Exerccio: Prove que para todas as listas finitas l e funes f, concat (map (map f ) l) =
map f (concat l).
4.9
Aplicao parcial
Uma caracterstica importante de Haskell e que proporciona uma forma elegante e poderosa de
construo de funes a avaliao parcial que consiste na aplicao de uma funo a menos
argumentos que ela realmente precisa. Por exemplo, seja a funo multiplica que retorna o
produto de seus argumentos:
multiplica :: Int -> Int -> Int
multiplica a b = a * b
Esta funo foi declarada para ser usada com dois argumentos. No entanto, ela pode ser
chamada como multiplica 2. Esta aplicao retorna uma outra funo que, aplicada a um
argumento b, retorna o valor 2*b. Esta caracterstica o resultado do seguinte princpio em
Haskell: uma funo com n argumentos pode ser aplicada a r argumentos, onde r n. Como
exemplo, a funo dobraLista pode ser definida da seguinte forma:
dobraLista :: [Int] -> [Int]
dobraLista = map (multiplica 2)
onde multiplica 2 uma funo de inteiro para inteiro, a aplicao de multiplica a um de seus
argumentos, 2, em vez de ser aplicada aos dois. map (multiplica 2) uma funo do tipo [Int]
> [Int], dada pela aplicao parcial de map.
Como determinado o tipo de uma aplicao parcial? Pela regra do cancelamento: se uma
funo f tem o tipo t1 > t2 > ... > tn > t e aplicada aos argumentos e1 :: t1 , e2 ::
t2 , ...ek :: tk , com k n, ento o tipo do resultado dado pelo cancelamento dos tipos t1 at
tk , dando o tipo tk+1 > tk+2 > ...tn > t.
Por exemplo,
multiplica
multiplica
dobraLista
dobraLista
Mas afinal, quantos argumentos tem realmente uma funo em Haskell? Pelo exposto, a
resposta correta a esta questo 1 (UM). Isto currificao. Lembra-se do -clculo?
Isto significa que uma funo do tipo Int > Int > Int do mesmo tipo que Int
> (Int > Int). Neste ltimo caso, est explcito que esta funo pode ser aplicada a um
111
argumento inteiro e o seu resultado uma outra funo que recebe um inteiro e retorna outro
inteiro. Exemplificando,
multiplica
:: Int -> Int -> Int
multiplica 4 :: Int -> Int
multiply 4 5 :: Int
ou
Associatividade:
A aplicao de funo associativa esquerda, ou seja:
f a b = (f a) b
enquanto a flexa associativa pela direita:
t > u > v = t > (u > v).
4.9.1
Seo de operadores
Uma decorrncia direta das aplicaes parciais que representa uma ferramenta poderosa e elegante em algumas linguagens funcionais e, em particular, em Haskell, so as sees de operadores.
As sees so operaes parciais, normalmente relacionadas com as operaes aritmticas. Nas
aplicaes, elas so colocadas entre parnteses. Por exemplo,
(+2) a funo que adiciona algum argumento a 2,
(2+) a funo que adiciona 2 a algum argumento,
(>2) a funo que retorna True se um inteiro for maior que 2,
(3:) a funo que coloca o inteiro 3 na cabea de uma lista,
(++\n) a funo que coloca o caractere \n ao final de uma string.
Uma seo de um operador op coloca o argumento no lado que completa a aplicao. 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 funo inquilinos, definida no incio deste Captulo, normalmente, 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 seo da seguinte maneira, o que a torna
muito mais elegante.
inquilinos db pes = map snd (filter ((==pes) . fst) db).
Exemplo.
Este mais um exemplo que mostra o poder de expressividade de Haskell. Trata-se da criao
de um ndice remissivo, encontrado na maioria dos livros tcnicos. Este exemplo baseado no
112
livro de Simon Thompson [46]. Algumas funes j foram definidas anteriormente, no entanto
elas sero novamente definidas aqui para evitar ambigidades. Vamos mostrar, inicialmente, os
tipos de dados utilizados na simulao.
type Doc
type Linha
type Palavra
fazIndice ::
= String
= String
= String
Doc -> [ ([Int], Palavra) ]
Neste caso, o texto uma string como a seguinte, onde no sero colocados os acentos e os
tis para facilitar o entendimento:
doc :: Doc
doc = "Eu nao sou cachorro nao\nPra viver tao humilhado\nEu nao sou
cachorro nao\nPara ser tao desprezado"
Vamos utilizar este trecho e mostrar como ele vai ser transformado com a aplicao das
funes que sero definidas para realizar operaes sobre ele:
Dividir doc em linhas, ficando assim:
["Eu nao sou cachorro nao",
"Pra viver tao humilhado",
"Eu nao sou cachorro nao",
"Para ser tao desprezado"]
Esta operao realizada por divTudo :: Doc > [Linha]
Agora devemos emparelhar cada linha com seu nmero de linha:
[(1, "Eu nao sou cachorro nao"),
(2, "Pra viver tao humilhado"),
(3, "Eu nao sou cachorro nao"),
(4, "Para ser tao desprezado")]
Esta operao realizada por numLinhas :: [Linha] > [(Int, Linha)]
Temos agora que dividir as linhas em palavras, associando cada palavra com o nmero da
linha onde ela ocorre
[(1, Eu"), (1, "nao"), (1, "sou"), (1, "cachorro"), (1, "nao"),
(2, "Pra"), (2, "viver"), (2, "tao"), (2, "humilhado"),
(3, "Eu"), (3, "nao"), (3, "sou"), (3, "cachorro"), (3, "nao"),
(4, "Para"), (4, "ser"), (4, "tao"), (4, "desprezado")]
Esta operao realizada por todosNumPal :: [(Int, Linha)] > [(Int, Palavra)]
Agora necessrio ordenar esta lista em ordem alfabtica das palavras e, se a mesma
palavra ocorre em mais de uma linha, ordenar por linha.
[(1, "cachorro"), (3, "cachorro"), (4, "desprezado"), (1, "Eu"), (3, "Eu"),
(2, "humilhado"), (1, "nao"), (3, "nao"), (4, "Para"), (2, "Pra"), (4, "ser"),
(1, "sou"), (3, "sou"), (2, "tao"), (4, "tao"), (2, "viver")]
Esta operao realizada por ordenaLista :: [(Int, Palavra)] > [(Int, Palavra)]
Agora temos que modificar a lista de forma que cada palavra seja emparelhada com a lista
unitria das linhas onde ela ocorre:
[([1], "cachorro"), ([3], "cachorro"), ([4], "desprezado"), ([1], "Eu"), ([3], "Eu"),
([2], "humilhado"), ([1], "nao"), ([3], "nao"), ([4], "Para"), ([2], "Pra"), ([4], "ser"),
([1], "sou"), ([3], "sou"), ([2], "tao"), ([4], "tao"), ([2], "viver")]
Esta operao realizada por fazListas :: [(Int, Palavra)] > [([Int], Palavra)]
113
Agora devemos juntar as linhas que contm uma mesma palavra em uma mesma lista de
linhas
[([1,3], "cachorro"), ([4], "desprezado"), ([1,3], "Eu"), ([2], "humilhado"), ([1,3], "nao"),
([4], "Para"), ([2], "Pra"), ([4], "ser"), ([1,3], "sou"), (2,4], "tao"), ([2], "viver")]
Esta operao realizada por mistura :: [([Int], Palavra)] > [([Int], Palavra)]
Vamos agora diminuir a lista, removendo todas as entradas para palavras com menos de 3
letras
[([1,3], "cachorro"), ([4], "desprezado"), ([2], "humilhado"), ([1,3], "nao"),
([4], "Para"), ([2], "Pra"), ([4], "ser"), ([1,3], "sou"), ([2,4], "tao"), ([2], "viver")]
Esta operao realizada por diminui :: [([Int], Palavra)] > [([Int], Palavra)]
Usando composio avanada, para ficar mais claro o exemplo, temos:
fazIndice = divideTudo
numDeLinhas
todosNumPal
ordenaLista
fazListas
mistura
diminui
>.>
>.>
>.>
>.>
>.>
>.>
--------
Doc
[Linha]
[(Int, Linha)]
[(Int, Palavra)]
[(Int, Palavra)]
[([Int], Palavra)]
[([Int], Palavra)]
->
->
->
->
->
->
->
[Linha]
[(Int, Linha)]
[(Int, Palavra)]
[(Int, Palavra)]
[([Int], Palavra)]
[([Int], Palavra)]
[([Int], Palavra)]
importante notar que j existe no Prelude a funo lines, cujo resultado o mesmo da
funo formaLinhas. A definio de formaLinhas foi colocada aqui por motivos puramente
114
1
C
E
U
E
O
A
V
V
M
A
E
A
4
O
U
E
3
M
A
R
I
O
A
D
D
V
M
M
D
U
R
A
I
O
A
5
P
S
V
M
T
I
A
D
U
R
8
U
E
O
A
O
N
T
E
D
A
7
T
I
U
R
D
H
E
E
M
E
T
2
E
6
R
Q
T
P
A
A
U
S
E
E
A
C
P
U
Desta forma, a mensagem cifrada deve ser a seguinte, esclarecendo que cada espao em branco
tmbm considerado um caractere.
"EUEOAVVMAE A
AAU SEE
EME T EOAON TEDA
"
A MDURAIOAUE R IOADDVMSVMTIA
DURQTP
ACPU
IURDHE
O primeiro processamento a ser feito com a String original divid-la em palavras do tamanho
de cada coluna. Para isso construmos a funo div_em_col da seguinte forma:
div_em_col :: String -> Int -> [String]
div_em_col [ ] = [ ]
div_em_col str tam = (pega_col str tam) : div_em_col (tira_col str tam) tam
Esta funo utiliza as funes pega_col, que retorna uma palavra do tamanho de uma
coluna a partir de uma String, e a funo tira_col que retira uma palavra do tamanho de uma
coluna de uma String, retornando a nova String sem os caracteres referentes a esta palavra.
pega_col :: String -> Int -> String
pega_col [ ] _ = [ ]
pega_col (x : xs)
|tam > 0
= x : pega_col xs )tam - 1)
|otherwise = [ ]
tira_col :: String -> Int -> String
tira_col [ ] _ = [ ]
tira_col (x : xs) tam
|tam > 0
= tira_col xs (tam - 1)
|otherwise = (x : xs)
Os caracteres que formam a chave devem ser colocados em ordem crescente para se saber a
ordem em que cada coluna foi colocada na mensagem cifrada. Para isso ser utilizado o algoritmo
quicksort j implementado anteriormente neste Captulo. Utiliza-se a funo zip para construir
pares de cada letra da chave ordenada com os nmeros naturais que vo representar a posio
da coluna, a partir de 0 (zero).
117
***
*
***
*
***
121
O formato de cada nmero definido por uma lista de inteiros que indicam quantos *s se
repetem, seguidos de quantos brancos se repetem, at o final da matriz 5x3, comeando da
primeira linha at a 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 nmero nove composto por 4 *s (trs na primeira linha e um na segunda),
seguida de 1 espao, mais 4 *s, 2 espaos, 1 *, 2 espaos e 1 *. Construa funes para:
a. Dado o formato do nmero (lista de inteiros) gerar a string correspondente de *s e
espaos.
toString :: [Int] -> String
toString nove ==> "**** ****
*"
b. Faa uma funo que transforma a string de *s e espaos em uma lista de strings,
cada uma representando uma linha do LCD:
type Linha = String
toLinhas :: String -> [Linha]
toLinhas "**** **** * *"
==> ["***", "* *","***", "
*", "
*"]
c. Faa uma funo que pegue uma lista de strings e a transforme em uma nica string
com o caractere n entre elas:
showLinhas :: [Linha] -> String
showLinhas ["***", "* *", "***", "
==> "***\n* *\n***\n *\n
*", "
*"
*"]
d. Faa uma funo que pegue duas listas de linhas e transforme-as em uma nica lista
de linhas, onde as linhas originais se tornam uma nica, com um espao entre elas:
juntaLinhas :: [LInha] -> [Linha] -> [Linha]
juntaLInhas ["***", "* *", "***", " *", " *""]
["***", "* *", "***", " *"," *"]
==> ["*** ***", "* * * *", "*** ***",
" *
*", " *
*"]
e. Faa uma funo que, dado um inteiro, imprima-o usando *s, espaos e caracteres
ns. A funo tem que funcionar para inteiros de 2 e 3 dgitos.
tolcd :: Int -> String
Dica: use as funes div e mod de inteiros, a funo (!!) e a lista numeros, dada
a seguir:
122
numeros :: [[Int]]
numeros
= [zero, um, dois, tres, quatro, cinco, seis, sete, oito, nove]
f. Faa uma funo que, dada uma string de *s e espaos, retorne a representao da
string como [Int] no formato usado no LCD, ou seja, a funo inversa de toString.
toCompact :: String -> [Int]
toCompact "**** ***
*
*" = [4,1,4,2,1,2,1].
7. Defina, em Haskell, uma funo que aplicada a uma lista l e a um inteiro n, retorne todas
as sublistas de l com comprimento maior ou igual a n.
8. Voltando ao problema da criptografia resolvido neste Captulo, tente um novo procedimento
para criar um texto cifrado a partir de um texto pleno. Uma outra sugesto se refere
chave a ser utilizada. No caso mostrado, a chave no pode conter caracteres repetidos.
Imagine uma soluo para tratar chaves em que os dgitos possam ser repetidos.
9. Um nmero inteiro positivo conhecido como nmero de Mersenne se puder ser escrito
como Mn = 2n 1, para um nmero n inteiro no negativo. Existem nmeros de Mersenne
que so primos e outros que no so primos. Por exemplo, M1 = 1 (no primo), M2 = 3
(primo), M3 = 7 (primo), M4 = 15 (no primo), etc. O maior nmero primo de Mersenne
registrado at 30 de setembro de 2009 M46 = 243112609 1, com quase 13 milhes de
algarismos em sua representao decimal, e foi descoberto pelo Great Internet Mersenne
Prime Search. Para se ter idia da magnitude, para represent-lo em base decimal seriam
requeridas 3461 pginas com 50 linhas por pgina e 75 dgitos por linha. Defina em Haskell
uma funo que verifique se um dado nmero inteiro no negativo, x, , ou no, um primo
de Mersenne.
4.10
Resumo
Este Captulo foi dedicado inteiramente ao estudo das listas em Haskell, completando o estudo
dos tipos primitivos adotados em Haskell, alm do tipo estruturado produto cartesiano, representado pelas tuplas. Ele se tornou necessrio, dada a importncia que as listas tm nas linguagens
funcionais. Foi dada nfase s funes pr-definidas e s Compreenses, tambm conhecidas
por expresses ZF, mostrando a facilidade de construir funes com esta ferramenta da linguagem. Vimos tambm a elegncia, a facilidade e a adequao com que algumas funes, como
por exemplo, o quicksort, foram definidas. Finalmente foram vistas caractersticas importantes
na construo de funes como: polimorfismo, composio, avaliao parcial e currificao de
funes.
Apesar dos tipos de dados estudados at aqui j significarem um avano importante, Haskell
vai mais alm. A linguagem tambm permite a criao de tipos algbricos de dados e tambm
dos tipos abstratos de dados, temas a serem estudados nos prximos Captulos.
Grande parte deste estudo foi baseado nos livros de Simon Thompson [46] e de Richard Bird
[4]. Estas duas referncias representam o que de mais prtico existe relacionado com exerccios
usando listas. O livro de Paul Hudak [11] tambm uma fonte de consulta importante, dada a
sua aplicao multimdia.
Uma fonte importante de problemas que podem ser resolvidos utilizando as ferramentas aqui
mostradas o livro de Steven S. Skiena e Miguel A. Revilla [39], que apresenta um catlogo
dos mais variados tipos de problemas, desde um nvel inicial at problemas de solues mais
elaboradas. Outra fonte importante de problemas matemticos envolvendo grafos o livro de
Sriram Pemmaraju e Steven Skiena [31].
123
124
Captulo 5
5.1
Introduo
Este Captulo dedicado construo de um novo tipo de dado, diferente dos apresentados at
agora, conhecido como tipo algbrico de dados. Estes tipos de dados facilitam a simulao de
problemas de forma mais natural e mais prxima do mundo real. A presena destes tipos de
dados em Haskell possibilitam que ela seja uma linguagem utilizvel em vrias reas de aplicao.
Vamos iniciar este estudo com as classes de tipos, conhecidas mais comumente como type
class. As classes de tipos so utilizadas na implementao dos tipos algbricos e dos tipos
abstratos de dados, estes ltimos um tema a ser estudado em outro Captulo.
Os tipos algbricos, representam um aumento na expressividade e no poder de abstrao
de Haskell. Por este motivo, este estudo deve ser de domnio pleno para se aproveitar as possibilidades que a linguagem oferece, construindo bons programas. Como exemplo, as rvores
so estruturas importantes na modelagem de vrios problemas e podem ser implementadas em
Haskell de forma muito simples usando os tipos algbricos de dados. Esta constatao pode ser
verificada pelo usurio ao final deste Captulo.
5.2
Classes de tipos
J foram vistas funes que atuam sobre valores de mais de um tipo. Por exemplo, a funo
length, pr-definida em Haskell, pode ser empregada para determinar o tamanho de listas de
qualquer tipo. Assim, length uma funo polimrfica, ou seja, a mesma definio pode ser
aplicada a listas homogneas de qualquer tipo. Por outro lado, algumas funes podem ser
aplicadas a mais de um tipo de dados, mas tm de apresentar uma nova implementao para
cada tipo sobre o qual elas vo atuar. So os casos das funes +, - e *, por exemplo. O algoritmo
utilizado para somar dois valores inteiros deferente do algoritmo para somar dois valores reais,
uma vez que as representaes dos inteiros difernte da representao de valores reais. Estas
funes so sobrecarregadas.
Existe uma discusso antiga entre os pesquisadores sobre as vantagens e desvantagens de se
125
5.2.1
A funo elem, definida a seguir, quando aplicada a um elemento e a uma lista de valores do
tipo deste elemento, verifica se ele pertence ou no lista, retornando um valor booleano.
elem :: t -> [t] -> Bool
elem x [ ] = False
elem x (a : y) = x == a || elem x y
Analisando a definio desta funo aplicada lista no vazia, verificamos que feito um
teste para verificar se o elemento x igual a cabea da lista (x==a). Para este teste utilizada
a funo de igualdade (==). Isto implica que a funo elem s pode ser aplicada a tipos cujos
valores possam ser comparados pela funo ==. Os tipos que tm esta propriedade formam
uma classe em Haskell.
Em Haskell, existem dois operadores de teste de igualdade: == e /=. Para valores booleanos,
eles so definidos 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 diferena entre == e =. O smbolo == usado para denotar um
teste computacional para a igualdade, enquanto o smbolo = usado nas definies e no sentido
matemtico normal. Para a Matemtica, a assertiva double = square uma afirmao falsa
e a assertiva = verdadeira, uma vez que qualquer coisa igual a si prpria. No entanto,
para a Computao, as funes no podem ser testadas quanto a sua igualdade e o resultado
da avaliao == tambm e no True. Isto no quer dizer que o avaliador seja uma
mquina matemtica, mas que seu comportamento descrito por um conjunto limitado de regras
matemticas, escolhidas de forma que elas possam ser executadas por um computador [4].
O objetivo principal de se introduzir um teste de igualdade poder us-lo em uma variedade
de tipos distintos, no apenas no tipo Bool. Em outras palavras, desejamos que == e /= sejam
operadores sobrecarregados. Estas operaes sero implementadas de forma diferente para cada
par de tipos e a forma adequada de introduz-las declarar uma classe que contenha todos
os tipos para os quais == e /= vo ser definidas. Esta classe pr-definida em Haskell e
denominada Eq.
126
5.2.2
Analisemos agora a funo todosIguais que verifica se trs valores inteiros so iguais, ou no.
todosIguais :: Int -> Int -> Int -> Bool
todosIguais m n p = (m == n) && (n == p)
Observe, nesta definio, que no feita qualquer restrio que a obrigue a ser definida
somente para valores inteiros. A nica operao realizada com os elementos m, n e p uma
comparao entre eles atravs da funo de igualdade ==. Dessa forma, a funo todosIguais
pode ser aplicada a trs valores de um tipo t qualquer, desde que seus valores possam ser
comparados pela funo ==. Isto d a funo todosIguais um tipo mais geral, da seguinte
forma:
todosIguais :: Eq t => t -> t -> t -> Bool
significando que a funo todosIguais no pode ser aplicada apenas a valores inteiros, mas
tambm a valores de qualquer tipo que esteja na classe Eq, ou seja, para os quais seja definido
um teste de igualdade (==). A parte antes do sinal => chamada de contexto que, neste caso,
a classe Eq. A leitura deste novo tipo deve ser: se um tipo t est na classe Eq ento a funo
todosIguais tem o tipo t > t > t > Bool. Isto significa que ela pode ter os seguintes
tipos: Int > Int > Int > Bool, Char > Char > Char > Bool ou ainda (Int,
Bool) > (Int, Bool) > (Int, Bool) > Bool, entre outros, uma vez que todos estes
tipos tm uma funo == definida para seus valores.
Vejamos agora o que acontece se ao tentar aplicar a funo todosIguais a argumentos do
tipo funo, como por exemplo, da funo suc definida da seguinte forma:
suc :: Int -> Int
suc = (+1)
Vejamos o que acontece ao se chamar a funo todosIguais suc suc suc.
O resultado mostrado pelos compiladores Haskell ou Hugs ERROR: Int > Int is not
an instance of class Eq, significando que no existe uma definio do teste de igualdade para
funes do tipo Int > Int (o tipo de suc).
127
5.2.3
Assinaturas e instncias
Foi visto que o teste de igualdade (==) sobrecarregado, o que permite que ele seja utilizado em
uma variedade de tipos para os quais esteja definido, ou seja, para instncias da classe Eq. Ser
mostrado agora como as classes e instncias so declaradas, por exemplo, a classe Visible que
transforma cada valor em um String e d a ele um tamanho. Esta classe necessria porque
o sistema de entrada/sada de Haskell s permite a impresso de Strings, ou seja, para que
qualquer valor em Haskell seja impresso, necessrio que ele seja primeiro transformado em um
String, caso ainda no o seja.
class Visible t where
toString :: t -> String
size :: t -> Int
A definio inclui o nome da classe (Visible) e uma assinatura, que so as funes que
compem a classe juntamente com seus tipos. Um tipo t para pertencer classe Visible tem
de implementar as duas funes da assinatura, ou seja, coisas visveis so coisas que podem ser
transformadas em um String e que tenham um tamanho. Por exemplo, para se declarar que o
tipo Char seja uma instncia da classe Visible, deve-se fazer a declarao
instance Visible Char where
toString ch = [ch]
size _ = 1
que mostra como um caractere deve ser transformado em um String de tamanho 1 para que
ele seja visvel. De forma similar, para declararmos o tipo Bool como uma instncia da classe
Visible, temos de declarar
instance Visible Bool where
toString True = "True"
toString False = "False"
size _ = 1
Para que o tipo Bool seja uma instncia da classe Eq devemos fazer:
instance Eq Bool where
True == True = True
False == False = True
_ == _ = False
5.2.4
Classes derivadas
Uma classe em Haskell pode herdar as propriedades de outras classes, como nas linguagens
orientadas a objetos. Como exemplo, vamos observar a classe Ord que possui as operaes >,
>=, <, <=, max, min e compare, alm de herdar a operao == da classe Eq. Sua definio
feita da seguinte forma:
class Eq t => Ord t where
(<), (<=), (>), (>=) :: t -> t -> Bool
max, min :: t -> t -> t
compare :: t -> t -> Ordering
128
O tipo Ordering ser definido mais adiante, quando nos referirmos aos tipos algbricos de
dados. Neste caso, a classe Ord herda as operaes de Eq (no caso, == e /=). H a necessidade
de se definir pelo menos a funo < (pode ser outra) para ser utilizada na definio das outras
funes da assinatura. Estas definies formam o conjunto de declaraes a seguir:
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 um String. Podemos
declarar a funo vSort para estas operaes, da seguinte forma:
vSort = toString . iSort
onde toString e iSort so funes j definidas anteriormente. Para ordenar a lista necessrio
que ela seja composta de elementos que pertenam a um tipo t e que possam ser ordenados, ou
seja, pertenam a um tipo que esteja na classe Ord. Para converter o resultado em um String,
necessrio que a lista [t] pertena classe Visible. Desta forma, vSort deve herdar das classes
Ord e Visible e, portanto, tem o tipo
vSort :: (Ord t, Visible t) => [t] -> String
O caso em que um tipo herda de mais de uma classe conhecido na literatura como herana
mltipla. Esta propriedade implementada em Haskell, apesar de se verificar algumas discordncias entre os projetistas de linguagens em relao a este tema. A herana mltipla tambm pode
ocorrer em uma declarao de instncia 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 ento o par (a, b) tambm est. A
herana mltipla tambm pode ocorrer na definio de uma classe. Por exemplo,
class (Ord a, Visible a) => OrdVis a
significando que os elementos da classe OrdVis herdam as operaes das classes Ord e Visible.
Este o caso da declarao de uma classe que no contm assinatura, ou contm uma assinatura vazia. Para estar na classe OrdVis, um tipo deve semplesmente estar nas classes Ord e
Visible. Neste caso, a definio da funo vSort anterior poderia ser modificada para
vSort :: OrdVis t => [t] -> String
Exerccios
1. Como voc colocaria Bool, o tipo par (a, b) e o tipo tripla (a, b, c) como instncias do
tipo Visible?
2. Defina uma funo para converter um valor inteiro em uma String e mostre como Int
pode ser uma instncia de Visible.
3. Qual o tipo da funo compare x y = size x <= size y?
129
5.2.5
Haskell contm algumas classes pr-definidas. Nesta sub-seo, vamos ver algumas delas com
alguma explicao sobre a sua utilizao e definio.
A classe Eq
Esta classe, j mencionada anteriormente, composta pelos tipos cujos valores podem ser
comparados quanto a sua igualdade ou a sua diferena. Para isso, foram construdas as funes
de teste de igualdade, ==, e de diferena, /=, cujas definies so as seguintes:
class Eq t where
(==), (/=) :: t -> t -> Bool
x /= y = not (x == y)
x == y = not (x /= y)
A classe Ord
A classe Ord contm tipos, cujos valores podem ser ordenados, ou seja, podem ser testados
pelas funes <, , > e . Deve ser claro que os valores destes tipos tambm devem poder
ser testados quanto as suas igualdades e diferenas. Estes testes j so providos pela classe Eq,
portanto a classe Ord deve herdar estes testes da classe Eq. Assim, a classe Ord definida da
seguinte forma:
class (Eq t) => Ord t where
compare :: t -> t -> Ordering
(<), (<=), (>=), (>) :: t -> t -> Bool
max, min :: t -> t -> t
onde o tipo Ordering tem um entre trs valores possveis: LT, EQ e GT, que so os resultados
possveis de uma comparao entre dois valores. A definio de compare :
compare x y
|x == y
|x < y
|otherwise
= EQ
= LT
= GT
A vantagem de se usar a funo compare que muitas outras funes podem ser definidas
em funo 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
-----
[m ..]
[m, n ..]
[m .. n]
[m, m .. n]
As funes fromEnum e toEnum tm as funes ord e chr do tipo Char como correspondentes, ou seja ord e chr so definidas usando fromEnum e toEnum. Simon Thompson
[46] afirma que o Haskell report estabelece que as funes toEnum e fromEnum no so
significativas para todas as instncias da classe Enum. Para ele, o uso destas funes sobre
valores de ponto flutuante ou inteiros de preciso completa (Double ou Integer) resulta em
erro de execuo.
A classe Bounded
Esta uma classe que apresenta um valor mnimo e um valor mximo para a classe. Suas
instncias so Int, Char, Bool e Ordering. Sua definio a seguinte:
class Bounded t where
minBound, maxBound :: t
que retornam os valores mnimos e mximos de cada tipo.
A classe Show
Esta classe contm os tipos cujos valores podem ser escritos como String. Em Haskell,
apenas os Strings podem ser mostrados. Portanto, se um valor no for um String, necessrio
que ele seja transformado antes em um String para depois poder ser mostrado. A maioria dos
tipos pertence a esta classe. Sua definio 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 contm os tipos cujos valores podem ser lidos a partir de Strings. Para usar a
classe necessrio apenas conhecer a funo read :: Read t => String > t. Esta classe
complementa a classe Show, uma vez que os Strings produzidos por show so normalmente
possveis de serem lidos por read.
read :: Read t => String -> t
A Figura 5.1 mostra um resumo das principais classes incorporadas Biblioteca padro de
Haskell e a relao de herana existente entre elas, baseadas na referncia [36]. importante
salientar que muitas outras classes so continuamente incorporadas a esta biblioteca.
131
Show
Eq
Functor
showPrec
show
showList
(==) (/=)
fmap
Monad
Num
Ord
(>>=) (>>)
return
fail
(+) () (*)
negate
abs signum
fromInteger
compare
(<) (<=) (>=) (>)
max min
MonadPlus
Ix
range
index
inRange
rangeSize
(/)
recip
fromRational
fromDouble
toRational
Enum
succ pred
toEnum
fromEnum
enumFrom
enumFromThen
enumFromTo
enumFromThenTo
Integral
mZero
mPlus
Fractional
Real
RealFrac
properFraction
truncate
round
ceiling
floor
Read
readsPrec
readList
Bounded
minBound maxBond
Floating
pi
exp log sqrt
(**) logBase
sin cos tan
sinh cosh tanh
asinh acosh atanh
RealFloat
floatRadix
floatDigits
floatRange
decodeFloat
encodeFloat
exponent
significand
scaleFloat
isNaN isInfinite
isDenormalized
isNegativeZero
isIEEE
atan2
5.3
Tipos algbricos
J foram vistas vrias formas de modelar dados em Haskell. Vimos as funes de alta ordem,
polimorfismo, sobrecarga e as classes de tipos (type class). Vimos tambm que os dados podem
ser modelados atravs dos seguintes tipos:
Tipos bsicos (primitivos): Int, Integer, Float, Double, Bool, Char e listas.
Tipos compostos: tuplas (t1 , t2 , ..., tn ) e funes (t1 > t2 ), onde t1 e t2 so tipos.
Estas facilidades j mostram um grande poder de expressividade e de abstrao oferecidos
pela linguagem. No entanto, Haskell oferece uma forma especial para a construo de novos tipos
de dados visando modelar situaes peculiares. Como exemplo destas situaes podemos citar,
entre outras:
as enumeraes implementadas em algumas linguagens de programao;
tipos cujos elementos sejam nmeros ou Strings. Por exemplo, uma casa em uma rua pode
ser identificada por um nmero ou pelo nome da famlia - em alguns pases isto normal;
as estruturas de rvores;
as estruturas de grafos.
Para construir um novo tipo de dados, usamos a declarao data que descreve como os elementos deste novo tipo so construdos. Cada elemento nomeado por uma expresso formulada
em funo dos construtores do tipo. Alm disso, nomes diferentes denotam elementos tambm
distintos. Atravs do uso de pattern matching sobre os construtores, projetam-se operaes que
132
geram e processam elementos do tipo de dados escolhidos pelo programador. Os tipos assim
descritos, no as operaes, so chamados tipos concretos [46, 4] e so modelados em Haskell
atravs dos tipos algbricos.
5.3.1
Um tipo algbrico mais uma facilidade que Haskell oferece, proporcionando ao programador
maior poder de abstrao, necessrio para modelar estruturas de dados complexas. Outras
possibilidades tambm existem, como por exemplo, os tipos abstratos de dados a serem vistos
mais adiante. A sintaxe da declarao de um tipo algbrico de dados
data <Const_tipo> = <Const_Val1> | <Const_Val2> | ...
| <Const_Valn>
utilizados em qualquer contexto onde um valor inteiro seja esperado, representando uma fonte
de possv eis bugs indesejveis. Este no o caso dos tipos algbricos em Haskell. Por exemplo,
em C, o tipo enumerao dia_util pode ser definido da seguinte forma:
enum dia_util {segunda, terca, quarta, quinta, sexta};
Este tipo pode ser definido, em Haskell, como um tipo algbrico, com a vantagem da ausncia
dos bugs citados. Seno vejamos:
data Dia_util =
|
|
|
|
Segunda
Terca
Quarta
Quinta
Sexta
deriving (Eq, Show)
1. Sendo a Estao = {Primavera, Vero, Outono, Inverno} e Tempo = {Quente, Frio}, defina
a funo tempo :: Estacao > Tempo de forma a usar guardas em vez de pattern
matching.
2. Defina o tipo Meses como um tipo algbrico em Haskell. Faa uma funo que associe um
ms a sua Estao. Coloque ordenao sobre o tipo.
3. Defina uma funo que calcule o tamanho do permetro de uma forma geomtrica do tipo
Forma, definido no texto desta sub-seo.
4. Adicione um construtor extra ao tipo Forma para tringulos e estenda as funes area e
perimetro (exerccio anterior) para incluir os tringulos.
5.3.2
Tipos recursivos
Podemos usar o mesmo nome para o construtor do tipo e para o construtor de valores, j que
eles representam objetos distintos. Por exemplo, perfeitamente legal a construo do tipo data
Pessoa = Pessoa Nome Idade.
Exemplo. Uma expresso aritmtica simples que envolve apenas adies e subtraes de
valores inteiros pode ser modelada atravs de sua BNF. Usando um tipo algbrico podemos
modelar uma expresso da seguinte forma:
data Expr = Lit Int |Add Expr Expr |Sub Expr Expr
Alguns exemplos de utilizao deste tipo de dado podem ser:
2 modelado por Lit 2
2 + 3 modelado por Add (Lit 2) (Lit 3)
(3 1) + 3 modelado por Add (Sub (Lit 3) (Lit 1)) (Lit 3)
Podemos criar uma funo de avaliao que tome como argumento uma expresso e d como
resultado o valor da expresso. Para isso podemos construir a funo eval da seguinte forma:
eval
eval
eval
eval
:: Expr
(Lit n)
(Add e1
(Sub e1
-> Int
= n
e2) = (eval e1) + (eval e2)
e2) = (eval e1) - (eval e2)
Esta definio primitiva recursiva, ou seja, existe uma definio para um caso base (Lit n)
e uma definio recursiva para o passo indutivo. Por exemplo, uma funo que imprime uma
expresso pode ser feita da seguinte maneira:
mostraExpressao :: Expr -> String
mostraExpressao (Lit n) = show n
mostraExpressao (Add e1 e2)
= "(" ++ mostraExpressao e1 ++ "+" ++ mostraExpressao e2 ++ ")"
mostraExpressao (Sub e1 e2)
= "(" ++ mostraExpressao e1 ++ "-" ++ mostraExpressao e2 ++ ")"
Exerccios:
1. Construir uma funo que calcule o nmero de operadores em uma expresso.
2. Calcule:
136
5.3.3
As definies de tipos algbricos podem conter tipos variveis como t, u, etc. Por exemplo,
data Pares t = Par t t
e podemos ter
137
5.4
rvores
As listas so consideradas estruturas de dados lineares porque seus tens aparecem em uma
seqncia pr-determinada, ou seja, cada tem tem, no mximo, um sucessor imediato. Por
outro lado, as estruturas de dados, conhecidas como rvores, so estruturas no lineares porque
seus tens podem ter mais de um sucessor imediato.
As rvores podem ser utilizadas como representaes naturais para quaisquer formas de dados
organizados hierarquicamente, por exemplo, as estruturas sintticas das expresses aritmticas
ou funcionais. Na realidade, as rvores provem uma generalizao eficiente das listas como
formas de organizao e recuperao de informaes.
Existem diversos tipos de rvores, classificadas de acordo com a forma de sua bifurcao, com
a localizao de armazenamento das informaes, com o relacionamento entre estas informaes,
ou ainda, de acordo com o objetivo da rvore criada. Nesta seo, sero vistas apenas algumas
destas definies.
5.4.1
rvores binrias
Como o nome indica, uma rvore binria uma rvore que tem, no mximo, duas bifurcaes
para os seus tens. Podem existir vrias definies para estas rvores, no entanto, aqui ser
adotada a seguinte:
data ArvBin t = Folha t | No (ArvBin t) (ArvBin t)
Esta a definio de uma rvore binria onde cada elemento ou um n externo (uma
Folha com um valor do tipo t) ou um n interno, sem um valor armazenado, mas com duas
sub-rvores binrias como sucessoras para este n.
Uma sequncia de nmeros inteiros pode ser representada por vrias rvorres binrias distintas. Por exemplo, a partir da seqncia de nmeros inteiros 14, 09, 19 e 51 podem ser geradas
5 rvores binrias: 14 (09 (19 51)), 14 ((09 19) 51), (14 09) (19 51), (14 (09 19)) 51 e ((14 09)
19) 51, respectivamente mostradas graficamente na Figura 5.2.
5.4.2
Existem vrias medidas importantes das rvores em geral. Duas delas so a altura e o tamanho.
O tamanho de uma rvore se refere a quantidade de ns que detm informaes. No caso da
138
14
14
51
09
51
19
51
a)
09
14
19
09
51
14
19
b)
51
19
19
09
c)
14
d)
09
e)
Figura 5.2: Representao grfica das rvores binrias geradas pela seqncia 14, 09, 19, 51.
rvore binria definida desta forma, s existem informaes armazenadas nas folhas. Portanto,
o tamanho desta rvore a quantidade de suas folhas, tendo o mesmo papel que a funo length
exerce sobre as listas. Sua definio, em Haskell, :
tamArvBin :: ArvBin t -> Int
tamArvBin (Folha a) = 1
tamArvBin (No ae ad) = tamArvBin ae + tamArvBin ad
Por outro lado, a altura de uma rvore definida como a maior distncia, em nveis, entre a
raiz e qualquer folha da rvore. Sua definio, em Haskell, a seguinte:
altArvBin :: ArvBin t -> Int
altArvBin (Folha a) = 0
altArvBin (No ae ad) = 1 + max (altArvBin ae) (altArvBin ad)
onde a funo max predefinida em Haskell. Uma outra operao importante que pode ser
aplicada s rvores binrias transform-las em listas. Sua definio a seguinte:
arvBintolista :: ArvBin t -> [t]
arvBintolista (Folha a) = [a]
arvBintolista (No ae ad) = arvBintolista ae ++ arvBintolista ad
J foi observado que nas rvores binrias definidas desta forma um n ou uma folha ou
tem duas bifurcaes. Nestas rvores, existe uma relao importante entre a quantidade de ns
internos e externos, onde o nmero de ns externos sempre igual ao nmero de ns internos
mais 1. Os ns internos podem ser contados pela seguinte funo:
nosint :: ArvBin t -> Int
nosint (Folha a) = 0
nosint (No ae ad) = 1 + nosint ae + nosint ad
Uma outra caractertica importante das rvores a profundidade de cada um de seus ns,
que dada pela distncia, em nveis, que cada n tem da raiz da rvore. A profundidade da raiz
da rvore zero. A profundidade da rvore a maior das profundidades de suas folhas. Sua
definio a seguinte:
prof :: ArvBin t -> Int
prof (Folha a) = 0
prof (No ae ad) = 1 + max (prof ae) (prof ad)
Diz-se que uma rvore binria perfeita se todas as suas folhas tiverem a mesma profundidade, ou seja, se estiverem no mesmo nvel. Na Figura 5.2, mostrada anteriormente, apenas a
139
rvore representada na letra c) uma rvore binria perfeita. O tamanho de uma rvore binria
perfeita sempre uma potncia de 2 e existe exatamente uma rvore binria perfeita para cada
potncia de 2. Duas rvores binrias com o mesmo tamanho, no tm, necessariamente, a mesma
altura. Mesmo assim, estas medidas esto sempre relacionadas. O resultado a seguir verificado
em todas as rvores binrias finitas. Sua formulao a seguinte:
altura a < tamanho a 2altura a
A demonstrao desta propriedade feita utilizando o PIF (Princpio de Induo Finita)
sobre a altura h. A base da induo feita quando a rvore tem altura h = 0, ou seja, para
uma Folha a e o passo indutivo realizado admitindo-se que seja verdade para uma rvore do
tipo No ae ad e altura h, verificando a validade para uma rvore de altura h + 1.
Prova:
1. Caso (Folha a). Para este caso, temos:
altura (F olha a) = 0
tamanho (F olha a) = 1
altura (F olha a) < tamArvBin (F olha a)
tamArvBin (F olha a) = 1 = 20 = 2(altura (F olha a))
Concluso: como a assertiva vlida para o caso (Folha a) e para o caso (No ae ad), ento ela
vlida para toda rvore binria finita.
Dada qualquer lista finita xs de tamanho n, possvel construir uma rvore a com
arvBintolista a = xs e altArvBin a = dlog ne
onde dxe representa o menor inteiro que supera x.
Esta rvore tem uma altura mnima para a lista xs. Normalmente existem mais de uma
rvore de altura mnima para uma dada seqncia de valores. Estas rvores so importantes
porque o custo de recuperao de um valor mnimo, mesmo no pior caso.
Uma rvore de altura mnima pode ser obtida pela diviso da lista de entrada em duas
metades e, recursivamente, obtendo-se rvores de alturas mnimas para estas metades. A funo
fazArvBin, definida a seguir, constri esta rvore:
fazArvBin :: [t] -> ArvBin t
fazArvBin xs
= if m == 0
--se xs s tiver um elemento
then Folha (head xs)
else No (fazArvBin xe) (fazArvBin xd)
where (xe, xd) = (take m xs, drop m xs)
m = div (length xs) 2
140
5.4.3
As rvores binrias definidas na sub-seo anterior se caracterizam pelo fato de suas informaes
serem armazenadas apenas nas folhas. No entanto, possvel definir uma rvore binria em que
os ns internos tambm armazenem informaes. Estas rvores tm aplicaes importantes e
so conhecidas como rvores binrias aumentadas.
No nosso caso, vamos aumentar a rvore binria, colocando em cada n interno a informao
dos tamanhos das subrvores que eles representam. Para isto, vamos definir um novo tipo de
rvore binria, que ser denotada por ArvBinA t.
data ArvBinA t = Folha t | No Int (ArvBinA t) (ArvBinA t)
Nesta definio, os construtores dos valores (Folha e No) so os mesmos adotados na
definio do tipo ArvBin. No entanto, eles devem ser diferenciados em scripts em que os
dois tipos de rvores estejam presentes, para evitar ambigidade. Para rotular os ns internos
de uma rvore binria aumentada com valores inteiros representando os tamanhos das subrvores relacionadas a cada n, devemos redefinir a funo tamArvBin feita para a rvore binria
ArvBin t.
tamArvBinA :: ArvBinA t -> Int
tamArvBinA (Folha x) = 1
tamArvBinA (No n ae ad) = n
Os ns internos sero rotulados utilizando a funo rotula definida da seguinte forma:
rotula :: ArvBinA t -> ArvBinA t -> ArvBinA t
rotula ae ad = No n ae ad where n = tamArvBinA ae + tamArvBinA ad
A definio da funo fazArvBin tambm deve ser modificada para construir uma rvore
binria aumentada com altura mnima a partir de uma lista xs. Sua definio passa a ser:
fazArvBinA :: [t] -> ArvBinA t
fazArvBinA xs
|(m==0)
= Folha (head xs)
--se a lista xs for unitria
|otherwise = rotula (fazArvBinA xe) (fazArvBinA xd)
where m = div (length xs) 2
(xe, xd) = (take m xs, drop m xs)
A funo arvtolista, que transforma uma rvore binria em uma lista, tambm tem de
ser modificada para transformar uma rvore binria aumentada em uma lista. Ela deve ter a
seguinte implementao:
arvBinAtoLista :: ArvBinA t -> [t]
arvBinAtoLista (Folha x) = [x]
arvBinAtoLista (No n ae ad) = (arvBinAtoLista ae) ++ (arvBinAtoLista ad)
Podemos agora implementar uma funo recupera que retorna o valor de um determinado
n da rvore. Isto pode ser feito transformando-se a rvore em uma lista, aplicando em seguida
a funo !! que recupera o k-simo valor desta lista.
recupera :: ArvBinA t -> Int -> t
recupera a k = (arvBinAtoLista a)!!k
141
A funo recupera indexa a rvore da mesma forma que a funo !! indexa uma lista, ou
seja, ela retorna o valor especificado em um n, enquanto !! recupera o k-simo valor de uma
lista. Podemos tambm usar a relao a seguir que verdadeira sobre listas:
(xs ++ ys)!!k = if k<m then xs!!k else ys!!(k-m)
where m = length xs
Agora podemos implementar a funo recupera de forma mais eficiente, transformando-a
em recupABA da seguinte maneira:
recupABA :: ArvBinA t -> Int -> t
recupABA (Folha a) 0 = a
recupABA (No m ae ad) k
= if k < m then recupABA ae k else recupABA ad (k-m)
Esta nova definio, recupABA, usa a informao do tamanho da subrvore que est armazenada nos ns internos para controlar a busca por um elemento, em uma dada posio. Esta
implementao mais eficiente que a anterior que transforma a rvore toda em uma lista e depois
usa !! para recuperar um valor e esta busca proporcional ao tamanho da lista.
Exerccios (baseados em Richard Bird [4]).
1. Quantas rvores existem de tamanho 5? Escreva um programa para calcular o nmero de
rvores binrias de tamanho n, para um dado n N.
2. Prove que o nmero de folhas em uma rvore binria sempre igual a 1 mais o nmero de
ns internos.
3. As subrvores de uma rvore binria podem ser definidas por
subarvores :: ArvBin t -> [ArvBin t]
subarvores (Folha x) = [Folha x]
subarvores (No xt yt) = [No xt yt] ++ subarvores xt ++ subarvores yt
Estabelea e prove o relacionamento entre length(subarvores xt) e tamanho xt.
4. A rvore aumentada tambm pode ser construda inserindo-se em cada n interno a informao da profundidade deste n. Faa esta definio.
5.4.4
As rvores binrias tambm podem ser definidas levando em considerao a existncia de uma
rvore nula. Este tipo de rvore conhecida como rvore de busca binria. Sua definio pode
ser feita da seguinte forma:
data (Ord t) => ArvBusBin t = Nil | No (ArvBusBin t) t (ArvBusBin t)
Este o exemplo de uma declarao de tipo de dado que utiliza um contexto, ou seja, a
rvore do tipo ArvBusBin t definida para tipos t que sejam instncias da classe Ord. Isto
significa que todos os valores dos ns internos desta rvore podem ser comparados pelas relaes
maior, maior ou igual, menor, menor ou igual e igual. Como pode ser observado, esta definio
no diferencia ns internos de externos porque no contempla uma definio de Folha e as informaes armazenadas nos ns aparecem entre as subrvores componentes. O construtor Nil
142
representa a rvore vazia ou nula. Este novo tipo de rvore tambm conhecida como rvore
binria rotulada. As funes arvtolista e arvBintolista, definidas anteriormente, que transformam as rvores binrias e binrias aumentadas em listas podem ser facilmente modificadas
para refletir as alteraes da nova definio. Ela ser definida de forma a retornar a lista ordenada dos rtulos da rvore, sendo este o motivo pelo qual os elementos da nova rvore devem
pertencer a tipos cujos valores possam ser ordenados. Neste caso, os elemantos sero ordenados
na forma in-ordem.
arvBBtolista :: (Ord t) => ArvBusBin t -> [t]
arvBBtolista Nil = [ ]
arvBBtolista (No ae n ad) = arvBBtolista ae ++ [n] ++ arvBBtolista ad
Como o nome sugere, as rvores de busca binrias so utilizadas para realizar buscas de
forma eficiente. A funo membroABB que determina se um determinado valor aparece, ou
no, como o rtulo de algum n pode ser definida da seguinte forma:
membroABB :: (Ord t) => ArvBusBin t -> Bool
membroABB x Nil = False
membroABB x (No ae n ad)
|(x<n)
= membroABB x ae
|(x==n)
= True
|(x>n)
= membroABB x ad
No pior caso, o custo de avaliar membroABB x abb proporcional altura de abb, onde
a definio de altArvBin deve ser modificada para altABB, da seguinte forma:
altABB :: (Ord t) => ArvBusBin t -> Int
altABB Nil
= 0
altABB (No ae n ad)
|(ae == Nil) && (ad == Nil) = 0
|otherwise = 1 + maximo (altABB ae) (altABB ad)
Uma rvore de busca binria de tamanho n e altura h satisfaz a seguinte relao:
dlog(n + 1)e h < n + 1
Uma rvore de busca binria pode ser construda a partir de uma lista de valores. Para
isto, necessrio modificar a definio da funo fazArvBin, transformando-a em fazABB, da
seguinte forma:
fazABB :: (Ord t) => [t] -> ArvBusBin t
fazABB [ ]
= Nil
fazABB (x:xs) = No (fazABB ys) x (fazABB zs)
where (ys, zs) = particao (<=x) xs
onde a funo particao definida por:
particao :: (t -> Bool) -> [t] -> ([t], [t])
particao p xs = (filter p xs, filter (not.p) xs)
A diferena entre as funes fazABB e fazArvBin que fazABB no garante que a rvore
resultante tenha altura mnima. Por exemplo, o valor de x na segunda equao de fazABB
pode ser menor que qualquer elemento de xs, possibilitando que fazABB construa uma rvore
143
em que a subrvore esquerda seja nula. Enquanto a definio de fazArvBin pode ser executada
em tempo linear, a implementao mais eficiente de fazABB requer, pelo menos, n log n passos,
quando ambas so aplicadas a uma lista de tamanho n.
A definio da funo fazABB pode ser utilizada para implementar uma funo sort para
ordenar listas.
sort :: (Ord t) => [t] -> [t]
sort = arvBBtolista . fazABB
Se for eliminada a rvore intermediria desta definio, ela se transforma em uma definio
da funo quicksort, bastante utilizada na ordenao de listas.
Insero e remoo em rvores de busca binria
As rvores de busca binria podem ser utilizadas na implementao eficiente dos conjuntos como
um tipo de dados abstratos a ser definido no prximo Captulo. Para isto necessrio definir as
funes de insero e remoo de tens nestas rvores. A funo de insero ser insere:
insere :: (Ord t) => t -> ArvBusBin t -> ArvBusBin t
insere x Nil = No Nil x Nil
insere x (No ae n ad)
|(x<n) = No (insere x ae) n ad
|(x==n) = No ae n ad
|(x>n) = No ae n (insere x ad)
Nesta definio, pode-se observar que, se um tem j estiver na rvore, ele no ser mais
includo. A funo de remoo um pouco mais complexa porque ao se retirar um n interno
da rvore necessrio juntar as duas sub-rvores deste n. A funo de remoo ser definida
como remove:
remove :: (Ord t) => t -> ArvBusBin t -> ArvBusBin t
remove x Nil = Nil
remove x (No ae n ad)
|(x<n) = No (remove x ae) n ad
|(x>n) = No ae n (remove x ad)
|(x==n) = junta ae ad
A funo auxiliar junta ae ad tem a responsabilidade de unir as duas subrvores ae e ad,
satisfazendo a seguinte propriedade:
arvBBtolista (junta ae ad) = arvBBtolista ae ++ arvBBtolista ad
A juno das sub-rvores ae e ad requer que seja escolhido um valor para ser a nova raiz da
rvore. As escolhas possveis so o maior valor da sub-rvore ae ou o menor valor da sub-rvore
ad. No nosso caso, escolhemos o menor valor da sub-rvore da direita, ad, que deve ser retirado
de sua posio inicial para ocupar a posio da raiz que foi removida. Esta juno pode ser feita
da seguinte forma:
junta
junta
junta
junta
5.4.5
Vamos agora definir um outro tipo de rvore binria conhecida como rvore heap binria, cujo
tipo ser ArvHB.
data (Ord t) => ArvHB t = Nil |No t (ArvHB t) (ArvHB t)
O tipo ArvHB t virtualmente igual ao tipo ArvBusBin t, vista na sub-seo anterior,
com a diferena de que o rtulo em cada n em uma rvore heap binria colocado antes das
subrvores esquerda e direita e no entre elas. Alm disso, a rvore ArvHB t deve satisfazer
condio de que o rtulo em cada n no seja maior que os rtulos de qualquer das suas
subrvores. Isto significa que os rtulos, ao longo de qualquer caminho, a partir da raiz, esto
sempre em ordem no decrescente.
Para formalizar esta condio, vamos definir inicialmente a funo arvHBtolista que transforma uma rvore heap binria em uma lista:
arvHBtolista :: (Ord t) => ArvHB t -> [t]
arvHBtolista Nil = [ ]
arvHBtolista (No n ae ad) = n : merge (arvHBtolista ae) (arvHBtolista ad)
merge
merge
merge
merge
As rvores heap binrias podem ser utilizadas para propsitos diferentes dos indicados para
as rvores de busca binrias, ou seja, para operaes que no sejam de remoo, insero ou
145
pertinncia. As rvores heap binrias so mais adequadas para tarefas, por exemplo, encontrar
o menor rtulo em uma heap que pode ser feito em tempo constante. Esta operao realizada
em uma rvore de busca binria requer tempo proporcional distncia ao n mais esquerda.
Enquanto a juno de duas rvores de busca binrias uma tarefa complexa, a juno de duas
rvores heaps binrias simples.
Construo de heaps
Uma forma de construir uma rvore heap binria definir uma nova verso da funo fazABB,
transformando-a na funo fazHB.
fazHB :: (Ord t) => [t] -> ArvHB t
fazHB [ ] = Nil
fazHM (x : xs) = No x (fazHB ys) (fazHB zs)
where (ys, zs) = particao (<x) xs
Deve ser notado que, apesar do programa ser bastante pequeno, ele no representa a forma
mais eficiente de construo de heaps. A idia definir fazHeap como uma composio de
funes auxiliares:
fazHeap :: (Ord t) => [t] -> ArvHB t
fazHeap = heapfica . fazHB
A funo fazHB constri uma rvore de altura mnima, no necessariamente ordenada, e a
funo heapfica reorganiza os rtulos para garantir a ordenao.
heapfica :: (Ord t) => ArvHB t -> ArvHB t
heapfica Nil = Nil
heapfica (No n ae ad) = desliza n (heapfica ae) (heapfica ad)
A funo desliza toma um rtulo e duas rvores heap binrias e constri uma nova rvore
heap binria, deslizando o rtulo at que a propriedade de heap seja estabelecida.
desliza :: (Ord t) => t -> ArvHB t -> ArvHB t -> ArvHB t
desliza x Nil Nil = No x Nil Nil
desliza x (No y aee aed) Nil = if x <= y then No x (No y aee aed) Nil
else No y (desliza x aee aed) Nil
desliza x Nil (No z ade add) = if x <= z then No x Nil (No z ade add)
else No z Nil (desliza x ade add)
desliza x (No y aee aed) (No z ade add)
|x <= (min y z) = No x (No y aee aed) (No z ade add)
|y <= (min x z) = No y (desliza x aed aed) (No z ade add)
|z <= (min x y) = No z (No y aed aed) (desliza x ade add)
Finalmente, podemos usar rvores heap binrias para obter uma nova implementao para o
algoritmo de ordenao, conhecido como heapsort:
heapsort :: (Ord t) => [t] -> [t]
heapsort = arvHBtolista.fazHeap
Exerccios (baseados em Richard Bird [4])
146
1. Defina uma funo permuta que troca a ordem de uma unio. permuta :: Uniao t u
> Uniao u t. Qual ser o efeito da aplicao (twist . twist)?
2. Uma forma de construir uma rvore binria a partir de uma lista [x0 , x1 , x2 , . . .] feita
da seguinte forma: x0 ser o rtulo da raiz da rvore, x1 e x2 sero os rtulos dos filhos,
x3 , x4 , x5 e x6 sero os rtulos dos netos e assim sucessivamente. Esta uma forma de
diviso de uma lista em sub-listas cujos tamanhos sejam potncias de 2 pode ser implementada atravs da funo niveis, definida por
niveis :: [t] -> [[t]]
niveis = niveisCom 1
Defina a funo niveisCom.
3. Podemos tambm definir rvores mais gerais com uma lista arbitrria de sub-rvores. Por
exemplo,
data ArvoreGeral t = Folha t | No [ArvoreGeral t]
defina funes para:
(a) contar o nmero de folhas em uma ArvoreGeral,
(b) encontrar a profundidade de uma ArvoreGeral,
(c) somar os elementos de uma rvore genrica,
(d) verificar se um elemento est em uma ArvoreGeral,
(e) mapear uma funo sobre os elementos das folhas de uma ArvoreGeral e
(f) transformar uma ArvoreGeral em uma lista.
5.5
Tratamento de excees
Para construir bons programas, necessrio especificar o que o programa deve fazer no caso de
acontecer algumas situaes anmalas. Estas ocorrncias so conhecidas como excees, normalmente indesejveis, e podem ser de vrios tipos: a tentativa de diviso por zero, clculo de raiz
quadrada de nmero negativo ou a aplicao da funo fatorial a um nmero negativo, tentativa
de encontrar a cabea ou a cauda de uma lista vazia, entre outros.
Existem basicamente trs tcnicas paraa resolver estes tipos de problemas, conhecidas como
tcnicas de tratamento de erros. A soluo mais simples exibir uma mensagem informando o
tipo da exceo e parar a execuo do programa. Isto pode ser feito atravs de uma funo de
erro.
error :: String -> t
Uma tentativa de avaliar a expresso error "Circulo com raio negativo" resultar na
mensagem
Program error: Circulo com raio negativo.
que impressa e a execuo do programa abortada.
O problema com esta tcnica que todas as informaes usuais da computao calculadas
at o ponto em que ocorreu a exceo so perdidas, porque o programa abortado. Em vez
disso, o erro pode ser tratado de alguma forma, sem parar a execuo do programa. Isto pode
ser feito atravs das duas tcnicas a seguir.
147
5.5.1
Valores fictcios
A funo tail construda para retornar a cauda de uma lista finita no vazia e, se a lista for
vazia, reportar esta situao com uma mensagem de erro e parar a execuo. Ou seja,
tail :: [t] -> [t]
tail (a : x) = x
tail [ ] = error "cauda de lista vazia"
No entanto, esta definio poderia ser refeita da seguinte forma:
tl :: [a] -> [a]
tl (_:xs) = xs
tl [ ]
= [ ]
Desta forma, todas as listas passam a ter uma resposta para uma solicitao de sua cauda,
seja ela vazia ou no. Se a lista no for vazia, sua cauda ser reportada normalmente. Se a lista
for vazia, o resultado ser o valor fictcio
. De forma similar, a funo de diviso de dois nmeros inteiros pode ser feita da seguinte forma,
envolvendo o caso em que o denominador seja zero:
divide :: Int -> Int -> Int
divide n m
|(m /= 0) = n div m
|otherwise = 0
Para estes dois casos, a escolha dos valores fictcios bvia. No entanto, existem casos em
que esta escolha no simples, nem mesmo possvel. Por exemplo, na definio de uma funo
que retorne a cabea de uma lista. Qual deve ser o valor fictcio a ser adotado neste caso?
Para resolver excees deste tipo, temos de recorrer a um artifcio mais elaborado. A soluo
adotada definir a funo hd, acrescentando mais um parmetro.
hd :: a -> [a] -> a
hd y (x:_) = x
hd y [ ]
= y
Esta tcnica mais geral e consiste na definio de uma nova funo para o caso de ocorrer
uma exceo. Em nosso caso, a funo hd de um argumento foi modificada para ser aplicada a
dois argumentos. De maneira geral, constri-se uma funo com uma definio para o caso de
surgir uma exceo e outra definio para o caso em que ela no ocorra.
fErr y x
|cond
= y
|otherwise = f x
Esta tcnica funciona bem em muitos casos. O nico problema que no reportada nenhuma
mensagem informando a ocorrncia incomum. Uma outra abordagem, mais elaborada e desejvel,
consiste em processar a entrada indesejvel.
148
5.5.2
Tipos de erros
As tcnicas anteriores para se tratar as excees se baseiam no retorno de valores fictcios, caso
elas aconteam. No entanto, em Haskell, possvel se adotar uma outra bem mais elaborada.
Baseia-se no retorno de um valor erro como resultado. Isto feito atravs do tipo Maybe.
data Maybe t = Nothing | Just t
deriving (Eq, Ord, Read, Show)
Na realidade Maybe t o tipo t com um valor extra, Nothing, acrescentado. Vamos
redefinir a funo de diviso, divide, transformando-a em 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 funo f, devemos fazer
fErr x
| cond
= Nothing
| otherwise = Just (f x)
O resultado destas funes agora no so 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:
1. podemos repassar o erro atravs de uma funo, que o efeito da funo mapMaybe, a
ser vista a seguir ou
2. podemos segurar a exceo, que o papel da funo maybe, tambm definida a seguir.
A funo mapMaybe repassa o valor de um erro, atravs da aplicao de uma funo g.
Suponhamos que g seja uma funo do tipo a b e que estejamos tentando us-la como
operador sobre um tipo Maybe a. Caso o argumento seja Just x, g ser aplicada a x, ou seja,
g x do tipo b. Se, no entanto, o argumento for Nothing, ento Nothing o resultado.
mapMaybe :: (a -> b) -> Maybe a -> Maybe b
mapMaybe g Nothing = Nothing
mapMaybe g (Just x) = Just (g x)
Para segurar um erro e resolv-lo, deve-se retornar um resultado do tipo b, a partir de uma
entrada do tipo Maybe a. Neste caso, temos duas situaes:
no caso Just, aplica-se a funo g de a para b;
no caso Nothing, temos de apresentar o valor do tipo b que vai ser retornado.
A funo de alta ordem que realiza este objetivo maybe, cujos argumentos, n e f, so
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
149
Vmos acompanhar as funes mapMaybe e maybe em ao, nos exemplos que seguem.
No primeiro deles, a diviso por zero nos retorna Nothing que vai sendo empurrado para a
frente e retornar o valor 56.
maybe 56 (1+) (mapMaybe (*3) (errDiv 9 0))
= maybe 56 (1+) (mapMaybe (*3) Nothing)
= maybe 56 (1+) Nothing
= 56
No caso da diviso ser normal, o valor a ser retornado Just 9. Este resultado 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 tcnica que podemos definir o sistema sem gerenciar os erros e depois
adicionar um gerenciamento de erro usando as funes mapMaybe e maybe juntamente com
as funes modificadas para segurar o erro. Separar o problema em duas partes facilita a soluo
de cada uma delas e do todo.
Exerccio.
Defina uma funo process :: [Int] > Int > Int > Int de forma que process l n
m toma o n-simo e o m-simo elementos de uma lista l e retorna a sua soma. A funo deve
retornar zero se quaisquer dos nmeros n ou m no forem ndices da lista l. Para uma lista de
tamanho p, os ndices so: 0, 1, ..., p-1, inclusive.
5.6
Lazy evaluation
Vamos iniciar nossa discusso avaliando a expresso quadrado (5 + 7), onde a funo quadrado
foi definida no Captulo 3. Vamos seguir duas seqncias de reduo possveis para esta expresso.
A primeira delas a seguinte:
quadrado (5 + 7)
= quadrado 12
-- usando a definicao do operador +
= 12 x 12
-- usando a definicao da funcao quadrado
= 144
A segunda seqncia de reduo para a mesma expresso :
quadrado (5 + 7)
= (5 + 7) * (5 + 7) -- pela definicao da funcao quadrado
= 12 * (5 + 7)
-- pela definicao do operador +
= 12 * 12
-- pela definicao do operador +
= 144
Estas duas seqncias de redues ilustram duas polticas de escolha de reduo, conhecidas
como innermost e outermost, respectivamente. Na primeira delas, em cada passo de reduo, foi
escolhido o redex mais interno, ou seja, o redex que no continha qualquer outro redex interno a
150
ele. J na segunda seqncia de redues, optou-se pela escolha do redex mais externo, ou seja,
um redex que no estava contido em qualquer outro.
Vamos analisar mais um exemplo, utilizando as polticas de seqncias de reduo, agora
sendo aplicadas expresso fst (quadrado 5, quadrado 7). Usando a poltica innermost,
teremos a seguinte seqncia:
fst (quadrado 5, quadrado 7)
= fst (5 * 5, quadrado 7)
= fst (25, quadrado 7)
= fst (25, 7 * 7)
= fst (25, 49)
= 25
------
usando
usando
usando
usando
usando
a
a
a
a
a
definicao
definicao
definicao
definicao
definicao
da
do
da
do
da
funcao quadrado
operador *
funcao quadrado
operador *
funcao fst
Nesta seqncia de redues, foram utilizados 5 passos at chegar forma mormal. Nos
primeiros dois passos desta seqncia, verifica-se que existiam duas possibilidades de escolha: a
funo quadrado 5 ou a funo quadrado 7. Apesar de ambos obedecerem poltica innermost,
a escolhe recaiu sobre o redex mais esquerda, conhecido como leftmost.
Se, por outro lado, tivssemos escolhido a poltica outermost, a seqncia de redues seria
a seguinte:
fst (quadrado 5, quadrado 7)
= quadrado 5
-- usando a definicao da funcao fst
= 5 * 5
-- usando a definicao da funcao quadrado
= 25
-- usando a definicao do operador *
Usando esta poltica de reduo, foram utilizados apenas 3 passos at atingir a forma normal
da expresso inicial porque a avaliao da funo quadrado 7 foi evitada, apesar de, em ambas
as seqncias, o resultado ser o mesmo.
Qual a diferena entre estas duas polticas de redues? Para responder a esta questo, vamos
supor que a funo quadrado 7 fosse indefinida na expresso inicial. Neste caso, a primeira
seqncia de redues no terminaria, ao passo que a segunda, sim. Isto nos permite inferir que
se as duas seqncias de redues atingem forma normal, elas chegam ao mesmo resultado e
que, em alguns caso, a utilizao da poltica innermost pode no atingir sua forma normal.
Resumidamente, se uma expresso tiver uma forma normal, ela pode ser computacionalmente
atingida utilizando a poltica outermost, ou seja, pode-se construir um programa computacional
capaz de encontrar sua forma normal. Por este motivo, a poltica de redues outermost
tambm conhecida como ordem normal. O leitor deve recorrer ao Captulo 2 deste estudo e
reanalisar os teoremas de Church-Rosser que ali so aplicados ao -clculo.
Apesar desta caracterstica importante, devemos observar que, no primeiro exemplo mostrado
nesta seo, foram necessrios 4 passos, enquanto que utilizando a poltica innermost foram
necessrios apenas 3. No entanto, se na expresso a ser avaliada existirem funes onde algum
argumento ocorra de forma repetida possvel que este argumento seja ligado a uma expresso
grande e, neste caso, a poltica de reduo innermost apresenta um desempenho bem menor.
Na Introduo deste nosso estudo, foi afirmada a utilizao de grafos de reduo na representao de expresses, em vez de rvores. Nos grafos, as sub-expresses de uma expresso
podem ser compartilhadas o que no possvel utilizando rvores. Por exemplo, a expresso
(5 + 7) (5 + 7) pode ser representada pelo grafo da Figura 5.4.
Neste caso, cada ocorrncia da subexpresso (5 + 7) representada por um arco que um
ponteiro para uma mesma instncia de (5+7). A representao de expresses como grafos significa
151
(5 + 7)
((5 + )/ , (5 )/ )
sqrt (quadrado 5 4 * 1 * 3)
(2 * 1)
--linha
--linha
--linha
--linha
--linha
--linha
--linha
1
2
3
4
5
6
7
Nas linhas 2 e 3 deste script, a sub-expresso (9 - 3) ocorre duas vezes. Neste caso, o avaliador
de Haskell avalia esta sub-expresso apenas uma vez e guarda o resultado, na expectativa de que
152
ela seja novamente referenciada. Na segunda ocorrncia da sub-expresso, ela j est avaliada.
Como no -clculo, a ordem de avaliao sempre da esquerda para a direita, ou seja, leftmostoutermost. Na avaliao da funo eqFunc2, a seguir, o segundo argumento (eqFunc1 34 3)
no avaliado porque ele no necessrio para a aplicao da funo eqFunc2.
eqFunc2 a b
eqFunc2 (10
= (10 *
= 400 +
= 432
= a + 32
* 40) (eqFunc1 34 3)
40) + 32
32
Nas sub-sees a seguir so mostrados alguns exemplos de como as listas so construdas, levando
em considerao o mecanismo de avaliao lazy. Em particular, as listas potencialmente infinitas
apenas so possveis por causa da existncia deste mecanismo. interessante observar que
algumas definies so de fato bastante criativas, o que torna as linguagens funcionais bastante
atrativas aos programadores que gostam de utilizar formas inusitadas e criativas na construo
de programas.
5.6.1
Expresses ZF (revisadas)
Algumas aplicaes que usam a construo de listas por compreenso s podem ser completamente entendidas se conhecermos o sistema de avaliao, principalmente quando envolver a
criao de listas potencialmente infinitas.
Uma expresso ZF uma expresso com a seguinte sintaxe:
[e | q1 , ..., qk ], onde e uma expresso e cada qi um qualificador que tem uma entre duas
formas:
1. um gerador: p<- lExp, onde p um padro e lExp uma expresso do tipo lista ou
2. um teste: bExp, onde bExp uma expresso booleana, tambm 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 ltimo exemplo, a ordem em que as triplas foram geradas. Ele
pega o primeiro nmero do primeiro gerador, em seguida o primeiro gerador do segundo, depois
percorre todos os valores do terceiro gerador. Quando todos 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. Aps se exaurirem todos os elementos do segundo
gerador ele volta para o segundo elemento do primeiro gerador e assim prossegue at o final.
Seqncia de escolhas
153
Para mostrar a seqncia de atividades, vamos utilizar a notao do -clculo para redues. Seja e uma expresso, portanto e{f/x} a expresso e onde as ocorrncias de x
so 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 + sum(x)){(2, [3, 4])/(a, x)} = 2 + sum[3, 4] .
Regras de teste
Na utilizao desta sintaxe necessrio que algumas regras de aplicao sejam utilizadas.
Estas regras so importantes para entender porque algumas aplicaes, envolvendo a criao de
listas, no chegam aos resultados pretendidos.
[e | T rue, q2 , ..., qk ] = [e | q2 , ..., qk ],
[e | F alse, q2 , ..., qk ] = [ ]
[e |] = [e] .
Exemplos
Vamos mostrar a seqncia de operaes em alguns exemplos, para ficar mais claro:
[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. Faa o clculo da expresso [a + b | a <- [1..4], b <- [2..4], a < b].
5.6.2
Suponhamos que seja necessrio encontrar a soma das quartas potncias dos nmeros 1 a n. Os
passos a seguir sero necessrios para resolver este problema:
construir a lista [1..n],
elevar quarta potncia cada nmero da lista, gerando a lista [1, 16, ..., n4 ] e
encontrar a soma dos elementos desta lista: = 1 + 16 + ... + n4 .
Desta forma a funo somaQuartaPotencia pode ser definida da seguinte forma:
somaQuartaPotencia n = sum (map (^4) [1..n])
= sum (map (^4)(1 : [2..n]))
= sum (((^4) 1) : map(^4) [2..n])
154
=
=
=
=
=
=
=
=
sum
sum
1 +
1 +
1 +
1 +
1 +
1 +
Deve ser observado que a lista no criada toda de uma s vez. To logo uma cabea
encontrada, toma-se a sua quarta potncia e em seguida aplicada a soma com algum outro
fator que vai surgir em seguida.
5.6.3
Listas infinitas no tm sido utilizadas na maioria das linguagens de programao. Na realidade, as listas infinitas no podem ser implementadas em qualquer lingaugem de programao,
uma vez que a memria finita. O que realmente algumas linguagens implementam, incluindo
Haskell, so listas potencialmente infinitas. Mesmo nas linguagens funcionais, elas s podem ser
implementadas se a linguagem utilizar lazy evaluation. Vejamos um exemplo.
uns :: [Int]
uns = 1 : uns
5.7
Com os tipos algbricos tambm podemos realizar provas sobre a corretude ou no de alguns
programas, de forma similar s provas com outros tipos de dados. Por exemplo, reformulando a
definio de rvore binria, 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 rvores finitas do tipo Arvore t, temos
de analisar dois casos:
1. o caso Nil: verificar se P(Nil) verdadeira e
2. o caso No: verificar se P(No x tr1 tr2) para todo x, assumindo P(tr1) e P(tr2).
Exemplo: provar que map f (collapse tr) = collapse (mapTree f tr).
Para provar isto devemos usar as seguintes definies:
map f [ ] = [ ]
map (a : x) = f a : map f x
(1)
(2)
(3)
collapse Nil = [ ]
collapse (No x t1 t2 )
= collapse t1 ++ [x] ++ collapse t2
(4)
(5)
(6)
156
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)
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)
por (6)
por (7 e 8)
5.8
Resumo
Este Captulo foi dedicado ao estudo de vrios temas. No entanto o objetivo maior foi analisar
os tipos de dados complexos, em particular, os tipos algbricos de dados. Foi visto como eles
157
podem ser construdos em Haskell para que o leitor possa segu-los e compreender como eles
podem modelar problemas. Na realidade, programar simular problemas construindo modelos
para estes problemas, de forma que estes modelos possam ser processados por um computador,
emitindo solues que possam ser interpretadas como solues para estes problemas.
Para possibilitar o uso destes tipos de dados, uma gama de ferramentas foram construdas,
em Haskell. Entre elas a utilizao de mdulos, o mecanismo de avaliao lazy e as compreenses.
Dadas as possibilidades de construo de tipos que a linguagem oferece, o objetivo do Captulo
foi mostrar uma grande gama de problemas em cujas solues a linguagem Haskell pode ser
aplicada com sucesso.
A grande fonte de exemplos e exerccios mostrados neste Captulo, foram os livros de Simon
Thompson [46] e Richard Bird [4]. No entanto, um fonte importante de problemas a serem
resolvidos pode ser o livro de Steven Skiena [39] e os sites:
http://www.programming-challenges.com e
http://online-judge.uva.es.
As descries de rvores AVL, B, B+, B*, red-black e outros tipos podem ser encontradas
na bibliografia dedicada aos temas Algoritmos e Estruturas de Dados. O leitor aconselhado a
consultar.
Para quem deseja conhecer mais aplicaes de programas funcionais, o livro de Paul Hudak
[11] uma excelente fonte de estudo, principalmente para quem deseja conhecer aplicaes da
multimdia usando Haskell.
158
Captulo 6
6.1
Introduo
No Captulo introdutrio desta Apostila, afirmamos que a principal vantagem das linguagens estruturadas era a modularidade oferecida por elas e, por este motivo, as linguagens funcionais eram
indicadas como soluo para a crise do software dos anos 80. De fato, as linguagens funcionais
so modulares e Haskell oferece muitas alternativas para a construo de mdulos, obedecendo
s exigncias preconizadas pela Engenharia de Software para a construo de programas.
Para John Hughes [13], a modularidade uma caracterstica que as linguagens de programao devem apresentar para que sejam utilizadas com sucesso, na construo de grandes sistemas. Ele afirma que esta a caracterstica principal das linguagens funcionais, proporcionada
atravs das funes de alta ordem e do mecanismo de avaliao lazy.
A modularidade importante porque:
as partes de um sistema podem ser construdas, compiladas e testadas separadamente e
pode-se construir grandes bibliotecas compostas de funes genricas, aumentando a reusabilidade.
No entanto algumas preocupaes devem ser levadas em considerao para que estes benefcios
sejam de fato auferidos. Caso contrrio, em vez de facilitar, a modularizao pode pode ser
tornar inoportuna. Entre os cuidados preconizados pela Engenharia de Software em relao aos
mdulos, so citados:
cada mdulo deve ter um papel claramente definido,
cada mdulo deve realizar exatamente uma nica tarefa,
159
6.2
Mdulos em Haskell
Nesta seo, fizemos um estudo rpido sobre a utilizao de mdulos em Haskell. O objetivo
mostrar as muitas possibiolidades que a linguagem oferece. A prtica da programao em
mdulos que vai dotar o programador da experincia e habilidade necessrias para o bom
desenvolvimento de programas.
Cada mdulo em Haskell constitui um arquivo. Isto significa que no devem existir um
arquivo em Haskell que contenha mais de um mdulo. Isto se deve ao fato de que o nome do
arquivo tem de ser o mesmo do mdulo, ambos iniciados com letra maiscula. Para declarar o
mdulo Formiga, devemos construir o arquivo Formiga.hs ou Formiga.lhs que deve conter
as seguintes declaraes:
module Formiga
( Formigas(..),
comeFormiga,
mataFormiga
) where
6.2.1
Controles de exportao
Por default, tudo o que declarado em um mdulo pode ser exportado, e apenas isto, ou seja, o
que importado de outro mdulo no pode ser exportado pelo mdulo importador. Esta regra
pode ser exageradamente permissiva porque pode ser que algumas funes auxiliares no devam
ser exportadas e, por outro lado, pode ser muito restritiva porque pode existir alguma situao
em que seja necessrio exportar algumas definies declaradas em outros mdulos.
Desta forma, deve-se poder controlar o que deve, ou no, ser exportado. Em Haskell, isto
declarado da seguinte forma:
module Abelha (Formigas(..), pegadordeAbelha, comeFormiga) where . . .
ou equivalentemente
module Abelha
(module Abelha, module Formiga) where . . .
A palavra module dentro dos parnteses significa que tudo dentro do mdulo exportado,
ou seja, module Abelha where equivalente a module Abelha (module Abelha) where.
6.2.2
Importao de mdulos
Os mdulos, em Haskell, podem importar dados e funes de outros mdulos da seguinte forma:
module Abelha where
import Formiga
pegadordeAbelha = . . .
As definies visveis no mdulo Formiga podem ser utilizadas no mdulo Abelha.
module Vaca where
import Abelha
Neste caso, as definies do tipo Formigas e da funo comeFormiga no so visveis
em Vaca. Elas podem se tornar visveis pela importao explcita de Formiga ou usando os
controles de exportao para modificar o que exportado a partir de Abelha.
6.2.3
Controles de importao
Alm dos controles para exportao, Haskell tambm apresenta controles para a importao de
definies.
module Tall where
import Formiga (Formigas (. . .))
Neste caso, a inteno importar apenas o tipo Formigas do mdulo Formiga. Haskell
tambm prov uma forma explcita de esconder alguma entidade. Por exemplo,
module Tall where
import Formiga hiding (comeFormiga)
161
6.3
Alm do interpretador GHCi, a distribuio GHC inclui tambm um compilador de cdigo nativo,
ghc. Para compilar um arquivo fonte necessrio abrir uma janela para linha de comandos e
chamar o ghc com o nome do arquivo a ser compilado, da seguinte forma
ghc -c Abelha.hs
O parmetro -c informa ao compilador ghc que ele deve gerar apenas o cdigo objeto para
o arquivo Abelha.hs. Se esta opo for omitida, o compilador ir tentar gerar um programa
executvel e no o programa objeto, o que ir provocar um erro porque ainda no foi definida
a funo main que GHC, obrigatoriamente, chama para iniciar a execuo de um programa
isolado.
Aps o compilador GHC concluir seu trabalho, ele gera os arquivos Abelha.o e Abelha.hi.
O arquivo Abelha.o contm cdigo de mquina e o arquivo Abelha.hi um arquivo de interface,
onde GHC armazena informaes sobre os nomes exportados pelo mdulo.
6.3.1
O mdulo Main
Em todo sistema deve existir um mdulo chamado Main que deve ser coficado no arquivo
Main.hs e deve conter a definio da funo main. Em um sistema interpretado, como Hugs,
ele tem pouca importncia porque um mdulo sem um nome explcito tratado como Main.
Para o caso em voga, ele pode ser codificado da seguinte forma:
module
import
import
import
main =
Main() where
Abelha
Formiga
Vaca
print (pegadordeAbelha.comeFormiga)
Aps ter compilado os mdulos com a diretiva -c, ou seja, aps terem sido criadas as interfaces
e os cdigos objetos dos mdulos, pode-se criar o arquivo executvel, que pode ter um nome
qualquer. Neste caso, foi escolhido o nome bicho para o programa executvel. Isto pode ser
feito da seguinte forma:
162
6.4
Nas sees a seguir, sero feitas consideraes relacionadas aos tipos abstratos de dados
e as formas de implementao destes tipos em Haskell. Estas implementaes so analisadas
quanto aos desempenhos que elas apresentam. Para cada tipo abstrato de dados definido sero
analisadas verses com diferentes desempenhos. O Captulo tem incio com o estudo dos Mdulos
em Haskell.
6.5
Haskell, como linguagem funcional, prov um nvel de abstrao bem mais alto que as linguagens
imperativas. Este fato, implica que os programas funcionais sejam bem menores, em termos de
tamanho de cdigo, mais claros e mais rpidos de serem desenvolvidos que os programas imperativos. Isto contribui para um melhor entendimento do algoritmo que est sendo implementado,
alm de possibilitar que solues alternativas sejam exploradas mais rapidamente.
J foi fortemente mencionado que Haskell usa o mecanismo de avaliao lazy como default.
No entanto, a linguagem tambm dispe de um mecanismo para que a avaliao de funes seja
feita de forma estrita, conhecida como eager evaluation, ou avaliao gulosa. Esta metodologia
de passagem de parmetros conhecida como passagem por valor, o que torna os programas
bastante rpidos e eficientes. O problema que a avaliao gulosa pode levar, em alguns casos,
a um loop infinito, fazendo com que o programa nunca pre. Isto garantido pelo teorema de
Church-Rosser, j analisado nos captulos 1 e 2. J com a avaliao preguiosa, se o programa
tiver uma forma normal ela ser encontrada usando esta tcnica de avaliao. No entanto,
esta forma de avaliao pode exigir mais passos de execuo que a avaliao gulosa. Alguns
compiladores utilizam um mecanismo de verificao da adequao deste mtodo em funes.
Este mecanismo conhecido como strictness anylising [32].
O problema que a anlise de desempenho pode dar resultados diferentes se for usada a
avaliao estrita ou a avaliao lazy. A anlise de desempenho em programas estritos relativamente fcil, mas em programas lazy no tem se mostrado uma tarefa simples. Por este motivo,
a anlise da complexidade das funes ser feita de forma superficial e sem muita profundidade,
dada a dificuldade do tema e no ser este o objetivo deste estudo.
6.5.1
A funo reverse inverte a ordem dos elementos de uma lista de qualquer tipo. Ela pr-definida
em Haskell, e pode ser facilmente definida por
reverse :: [t] -> [t]
reverse [ ]
= [ ]
reverse (a : x) = reverse x ++ [a]
A despeito da simplicidade desta definio, ela padece de um srio problema de desempenho.
Verifiquemos quantos passos so necessrios para inverter uma lista l de n elementos, usando esta
definio. A funo chama a si mesma n vezes e, em cada uma destas chamadas, chama ++,
tail e head. As funes head e tail so primitivas, ou seja, exigem tempo constante para suas
execues. A operao ++ deve saltar para o final de seu primeiro argumento para concaten-lo
com o segundo. O tempo total de reverse dado pela soma
(n 1) + (n 2) + (n 3) + . . . + 1 + 0 =
n1
X
i=0
164
i=
n(n 1)
O(n2 )
2
6.6
O objetivo desta seo introduzir os tipos abstratos de dados (TAD) e o mecanismo provido por
Haskell para defin-los. De maneira geral, os tipos abstratos de dados diferem dos introduzidos
por uma declarao data, no sentido de que os TADs podem escolher a representao de seus
valores. Cada escolha de representao conduz a uma implementao diferente do tipo de dado
[46].
Os tipos abstratos so definidos de forma diferente da forma utilizada para definir os tipos
algbricos. Um tipo abstrato no definido pela nomeao de seus valores, mas pela nomeao
de suas operaes. Isto significa que a representao dos valores dos tipos abstratos no
conhecida. O que realmente de conhecimento pblico o conjunto de funes para manipular
o tipo. Por exemplo, Float um tipo abstrato em Haskell. Para ele, so exibidas operaes de
comparao e aritmticas alm de uma forma de exibio de seus valores, mas no se estabelece
como tais nmeros so representados pelo sistema de avaliao de Haskell, proibindo o uso de
165
casamento de padres. Em geral, o programador que usa um tipo abstrato no sabe como seus
elementos so representados. Tais tipos de abstrao so usuais quando mais de um programador
estiverem trabalhando em um mesmo projeto ou mesmo quando um mesmo programador estiver
trabalhando em um projeto no trivial. Isto permite que a representao seja trocada sem afetar
a validade dos scripts que usam o tipo abstrato. Vamos mostrar estes fundamentos atravs de
exemplos.
6.6.1
Vamos dar incio ao estudo dos tipos abstratos de dados atravs da implementao do tipo Pilha.
As pilhas so estruturas de dados homogneas onde os valores so colocados e/ou retirados
utilizando uma estratgia LIFO (Last In First Out). Informalmente, esta estrutura de dados
comparada a uma pilha de pratos, na qual s se retira um prato por vez e sempre o que est
no topo da pilha. Tambm s se coloca um prato por vez e em cima do prato que se encontra
no topo.
As operaes1 necessrias para o funcionamento de uma pilha do tipo Stack, juntamente com
seus tipos, formam a assinatura do tipo abstrato. No caso em voga, a assinatura composta das
seguintes funes:
push
pop
top
stackEmpty
newStack
::
::
::
::
::
t -> Stack
Stack t ->
Stack t ->
Stack t ->
Stack t
da pilha
da pilha
pilha
vazia
Sero feitas duas implementaes do tipo pilha, sendo a primeira baseada em tipos algbricos
e a segunda baseada em listas.
Primeira implementao para o TAD Pilha
A implementao de um tipo abstrato de dados em Haskell feita atravs da criao de um
mdulo, tema estudado na seo anterior. O leitor deve voltar a ele se tiver alguma dificuldade
de entender as declaraes aqui feitas. Vejamos a primeira implementao, baseada em tipos
algbricos.
module Stack(Stack, push, pop, top, stackEmpty, newStack) where
push
:: t -> Stack t -> Stack t
pop
:: Stack t -> Stack t
top
:: Stack t -> t
stackEmpty :: Stack t -> Bool
newStack
:: Stack t
data Stack t = EmptyStk
| Stk t (Stack t)
push x s = Stk x s
pop EmptyStk
Apesar de preferirmos nomes em Portugus, nestes Exemplos, as operaes sero descritas com os nomes em
ingls por j fazerem parte do jargo da Computao.
166
pop (Stk _ s) = s
top EmptyStk = error "topo de uma pilha vazia"
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 utilizao do tipo abstrato Stack deve ser feita em outros mdulos cujas funes necessitam deste tipo de dado. Desta forma, o mdulo Stack deve constar na relao dos mdulos
importados pelo mdulo usurio. Por exemplo,
module Main where import Stack
listaParaPilha :: [t]
-> Stack t
listaParaPilha [ ]
= newStack
listaParaPilha (x : xs) = push x (listaParaPilha xs)
pilhaParaLista :: Stack t -> [t]
pilhaParaLista s
|stackEmpty s = [ ]
|otherwise
= (top s) : (pilhaParaLista (pop s))
Este script deve ser salvo no arquivo Main.hs e deve ser carregado para ser utilizado. Agora
podemos construir exemplos de algumas instncias do tipo abstrato Stack, da seguinte forma:
ex1 = push 14 (push 9 (push 19 (push 51 newStack)))
ex2 = push "Dunga" (push "Constantino")
A implementao mostrada foi baseada no tipo algbrico Stack t. No entanto, o implementador pode desenvolver uma outra implementao, mais eficiente que esta, e fazer a mudana
da implementao anterior para esta sem que os usurios precisem saber disto. Neste caso, as
funes do mdulo Main no precisam ser refeitas para serem utilizadas. O que no pode ser
modificada a interface com o usurio. Vamos construir a segunda implementao baseada em
listas finitas.
Segunda implementao para o TAD pilha
As operaes primitivas (assinatura) sero as mesmas da implementao anterior, ou seja, o que
muda apenas a implementao.
module Stack(Stack, push, pop, top, stackEmpty, newStack} where
push
:: t -> Stack t -> Stack t
pop
:: Stack t -> Stack t
top
:: Stack t -> t
stackEmpty :: Stack t -> Bool
167
newStack
:: Stack t
6.6.2
Vamos agora mostrar um segundo exemplo de tipo abstrato, o tipo Fila. Uma fila uma estrutura
de dados do tipo FIFO (First-In First-Out), onde os elementos s podem ser inseridos em
um lado e s podem ser retirados do outro (se existir algum elemento na fila), imitando uma fila
de espera. Podemos implementar o tipo Fila atravs de listas finitas, fazendo algumas restries
nas operaes. Para um usurio comum, importante uma implementao eficiente, mas ele no
est interessado como ela feita, normalmente solicitando a outra pessoa que a faa ou utiliza
uma soluo provida pelo sistema.
Da mesma forma feita para o tipo abstrato Pilha, precisamos conhecer que operaes primitivas so necessrias para compor a assinatura do tipo abstrato Queue t. Neste caso, so
elas:
enqueue
dequeue
front
queueEmpty
newQueue
::
::
::
::
::
t -> Queue
Queue t ->
Queue t ->
Queue t ->
Queue t
Vamos continuar o exemplo provendo duas implementaes do tipo abstrato Fila, da mesma
forma feita para o tipo Pilha.
Primeira implementao para o TAD Fila
A primeira implementao a ser mostrada baseada em uma lista finita. Neste caso, os valores
so colocados no final da lista e so retirados da cabea da lista. A implementao do mdulo
Queue com suas operaes feita da seguinte forma:
168
= Fila xs ys
= error "A fila esta vazia"
= Fila tail (reverse ys) [ ]
lista nica. O mdulo de utilizao pode ser o mesmo anterior, sem qualquer alterao, uma vez
que a implementao totalmente transparente para os usurios.
6.6.3
O tipo abstrato Set uma coleo homognea de elementos e implementa a noo de conjunto,
de acordo com a seguinte Interface:
emptySet
setEmpty
inSet
addSet
delSet
pickSet
::
::
::
::
::
::
Set
Set
(Eq
(Eq
(Eq
Set
t
--cria um conjunto vazio
t -> Bool
--testa se o conjunto eh vazio
t) => t -> Set t -> Bool --testa se um x estah em S
t) => t -> Set t -> Set t --coloca um item no conjunto
t) => t -> Set t -> Set t --remove um item de um conjunto
t -> t
--seleciona um item de S
::
::
::
::
::
::
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
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)
171
pickSet (S [ ])
= error "conjunto vazio"
pickSet (S (x : _)) = x
6.6.4
Uma tabela, Table a b, uma coleo de associaes entre chaves, do tipo a, com valores do
tipo b, implementando assim, uma funo finita, com domnio em a e co-domnio b, atravs de
uma determinada estrutura de dados.
O tipo abstrato Table pode ter a seguinte implementao:
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 =
6.7
Arrays
At este ponto, ainda no nos referimos implementao de arrays em Haskell. Estes tipos de
dados so muito teis na resoluo de problemas e merecem ser analisados como eles podem ser
utilizados nesta linguagem.
Os arrays, baseado em [34, 36], so usados para armazenar e recuperar um conjunto de
elementos, onde cada um deles tem um nico ndice, indicando a sua posio dentro do arranjo.
Nesta seo, ser descrito como estas estruturas podem ser utilizadas em Haskell. Na realidade,
o padro ainda em vigor para Haskell no contemplou estas estruturas e, por este motivo, elas
no fazem parte do Prelude padro. No entanto, foi criado um mdulo com a implementao
destas estruturas como um tipo abstrato de dados e pode ser importado.
6.7.1
A criao de arrays
x
:: Array Int Char
fun :: Int -> Array Int Int
y
:: Array (Int, Int) Int
Um array se torna indefinido se qualquer ndice for especificado fora de seus limites. Tambm,
se duas associaes tiverem o mesmo ndice far com que o valor do ndice seja indefinido. Isto
significa que um array estrito em seus limites e lazy em seus valores. Como exemplo, a funo
fib pode ser definida usando a funo array, da seguinte forma:
fib n = a
where a = array (1,n) ([(1,1),(2,1)] ++ [(i,a!(i-1)+a!(i-2))|i<-[3..n]])
onde o operador ! uma funo binria e infixa, de forma que a!i retorna o valor do elemento
de ndice i do array a.
A segunda funo pr-definida sobre arrays listArray, cuja sintaxe a seguinte:
listArray <limites> <lista_de_valores>
onde o argumento <limites> tem o mesmo significado j visto para a funo array e o argumento <lista_de_valores> a lista dos valores do array. Para exemplificar, o array x,
definido anteriormente, tambm pode ser construdo usando a funo listArray, da seguinte
forma:
x = listArray (1,5) "Dunga"
A terceira funo pr-definida, accumArray, tem a seguinte sintaxe:
accumArray <funcao> <init> <limites> <lista_de_associacoes>
Esta funo remove a restrio de que um dado ndice s possa aparecer, no mximo, uma
vez na lista de associaes, combinando os ndices conflitantes atravs da funo argumento
<funcao> e inicializando os valores do array com <init>. Os outros argumentos so os mesmos
j conhecidos para as duas funes anteriores. Por exemplo, o script a seguir mostra uma
chamada e o resultado utilizando esta funo:
Array>accumArray (-) 0 (1,5) [ ]
array (1,5) [(1,0),(2,0),(3,0),(4,0),(5,0)]
Um exemplo mais completo, pode ser
Array>accumArray (+) 2 (1,5) [(1,2),(2,3),(1,3),(4,1)]
array (1,5) [(1,7),(2,5),(3,2),(4,3),(5,2)]
em que todos os elementos do array so inicializados com o valor 2. Como no array existem dois
valores para o ndice 1, a funo (+) resolve esta indefinio somando os valores 2 e 3 ao valor
inicializado, totalizando 7. Os ndices 3 e 5 no constam na chamada, mas so inicializados com
o valor 2 que o segundo parmetro da funo.
6.7.2
Utilizando arrays
J foi visto que a funo ! retorna o valor do elemento mostrado cujo ndice o segundo
argumento. Outras funes tambm j existem para manipular arrays. So elas: bounds 174
exibe os limites de um array, indices - retorna os ndices do array, elems - retorna os elementos
do array e assocs que retorna as associaes do array. Sendo y o array definido na sub-seo
anterior, ento:
y!(1,2) => 2
bounds y => ((1,1),(2,2))
indices y => [(1,1),(1,2),(2,1),(2,2)]
elems y => [1,2,2,4]
assocs y => [((1,1),1),((1,2),2),((2,1),2),((2,2),4)]
Tambm possvel atualizar um array em um estilo funcional, ou seja, a partir de um
array podemos retorna um novo array cujos elementos so os mesmos do array anterior, exceto
para alguns ndices determinados. Esta operao feita atravs do operador // que toma
como argumentos um array e uma lista de associaes e retorna um novo array onde a lista de
substituies passadas substitui as associaes do array antigo. Por exemplo, sendo x o array
definido acima, a chamada x // [(1,F)] ter como resultado "Funga".
6.8
Resumo
Este Captulo foi reservado ao estudo dos mdulos e como eles podem ser construdos em Haskell.
Isto foi necessrio para que o usurio possa utilizar este recurso que torna possvel a programao
de grandes sistemas, de forma otimizada e gerencivel. A modularidade permite que os mdulos
sejam r-compilados e testados de forma independente, facilitando o gerenciamento na construo
de grandes programas.
Alm da modularidade, no Captulo tambm foram tratados os tipos abstratos de dados,
analisando a necessidade de seu surgimento, mostrando porque eles so abstratos em relao aos
tipos de dados vistos at aqui, que tm representaes concretas e acessveis aos usurios.
Foi visto como eles podem ser construdos em Haskell permitindo ao leitor que possa segu-los
e compreenda como outros tipos podem ser implementados, levando em considerao o desempenho de sua representao e de seus mtodos. J foi dito neste estudo que programar simular
problemas contruindo modelos para represent-los, de forma que estes modelos possam ser processados por um computador, emitindo solues que sejam interpretadas como solues para os
problemas simulados.
As fontes de exemplos e exerccios mostrados neste Captulo, foram os livros de Richard Bird
[4] e de Fethi Rabhi e Guy Lapalme [34].
Para quem deseja conhecer mais aplicaes de programas funcionais, o livro de Paul Hudak
[11] uma excelente fonte de estudo, principalmente para quem deseja conhecer aplicaes da
multimdia usando Haskell.
175
176
Captulo 7
7.1
Introduo
Este Captulo dedicado forma utilizada por Haskell para se comunicar com o mundo exterior
para realizar operaes de I/O que so inerentemente pertencentes ao mundo imperativo e,
portanto, possveis de conter efeitos colaterais. J foi visto anteriormente que, no paradigma
funcional, os programas so expresses a serem avaliadas para se encontrarem valores que so
atribudos a nomes. Em Haskell, o resultado de um programa o valor da avaliao de uma
funo que atribudo ao identificador main no Mdulo Main do arquivo Main.hs.
Mas a realidade que a grande maioria dos programas exige alguma interao com o mundo
externo. Por exemplo:
um programa pode necessitar ler ou escrever alguma entrada em algum terminal;
um sistema de e-mail l e escreve em arquivos ou em canais e
um programa pode querer mostrar uma figura em uma janela do monitor.
Historicamente, as operaes de I/O representaram um desafio muito grande durante muito
tempo para os usurios das linguagens funcionais. Os projetistas dessas linguagens tomaram
rumos distintos na soluo destes problemas. Por exemplo, os projetistas de Standard ML [30]
preferiram incluir operaes como
inputInt :: Int
cujo efeito a leitura de um valor inteiro a partir do dispositivo padro de entrada. Este
valor lido atribudo ao identificador inputInt. Mas surge um problema porque, cada vez
que inputInt avaliado, um novo valor a ele atribudo sendo esta uma caracterstica do
paradigma imperativo, no do modelo funcional. Por este motivo, diz-se que SML admite um
modelo funcional impuro, porque admite atribuies destrutivas.
Seja a seguinte definio de uma funo, em SML, que calcula a diferena entre dois inteiros:
177
7.1.1
O Comit de definio da linguagem Haskell resolveu manter a linguagem pura, sem side effects,
e, por este motivo, a deciso sobre o sistema de I/O foi um dilema a ser resolvido. Eles no
queriam perder o poder de expressividade da linguagem apenas porque ela tinha que ser pura
e a comunicao com o mundo externo era uma necessidade pragmtica. Havia o receio de que
Haskell passase a ser considerada uma linguagem de brincadeira se este tema fosse considerado
de pouca importncia em sua definio.
Inicialmente, duas solues foram propostas: streams e continuations (continuaes). Estes
dois temas j eram dominados teoricamente e pareciam oferecer as condies de expressividade
exigidas, alm de ambas serem puras. No decorrer dessas discusses, verificou-se que estas tcnicas eram funcionalmente equivalentes, ou seja, era possvel modelar streams usando continuaes
e vice-versa. Desta forma, no Haskell 1.0 Report, o sistema de I/O foi definido em termos de
streams, incluindo continuaes [12].
Em 1989, Eugenio Moggi publicou no LICS um artigo sobre o uso de Mnadas, originadas da
Teoria das Categorias [41] para descrever caractersticas das linguagens de programao. Philip
Wadler verificou que a tcnica que Moggi havia utilizado para estruturar semntica tambm
poderia ser empregada com sucesso para estruturar programas funcionais [48, 49].
Uma mnada consiste em um construtor de tipos M e um par de funes: return e >>=,
algumas vezes, esta ltima chamada de bind. Seus tipos so:
return :: a -> M a
(>>=) :: M a -> (a -> M b) -> M b
M a deve ser lido como o tipo de uma computao que retorna um valor do tipo a, com
possveis side effects. Digamos que m seja uma expresso do tipo M a e n seja uma expresso
do tipo M b com uma varivel livre, x, do tipo a. Assim, a expresso
m >>= (\x -> n)
tem o tipo M b. Isto significa que a computao indicada por m feita ligando-se o valor
retornado a x e em seguida realiza-se a computao indicada por n. Isto anlogo expresso
178
let x = m in n
em uma linguagem com side effects, como ML, excluindo o fato de que os tipos no indicam a
presena destes efeitos. Na verso de ML, m tem o tipo a em vez de M a e n tem o tipo b em
vez de M b.
Existem trs leis que as definies de return e >>= devem obedecer para que a estrutura
seja considerada uma mnada, no sentido definido pela teoria das categorias. Estas leis garantem
que a composio de funes com side effects seja associativa e tenha uma identidade [49].
Uma mnada um tipo de padro de programao. Este padro pode ser expresso em
Haskell usando type class da seguinte forma:
class Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
O estilo mondico rapidamente dominou os modelos anteriores. Os tipos so mais compactos
e mais informativos. Uma prova baseada em parametricidade garante que nenhuma referncia
desperdiada na mudana de uma computao encapsulada para outra [21]. O resultado prtico
disto foi que, pela primeira vez, esta prova ofereceu a possibilidade de se implementar uma funo
usando um algoritmo imperativo com a garantia da ausncia de qualquer side effect.
Em 1996, Peyton Jones e outros pesquisadores extenderam a mnada IO com threads, onde
cada thread pode realizar I/O, tornando a semntica da linguagem no determinstica [33]. Os
threads podem comunicar entre si usando locaes mutveis e sincronizadas chamadas MVars.
O leitor interessado em conhecer melhor este tema deve recorrer bibliografia especfica. Neste
particular, as referncias [48, 49] e [21] representam um bom comeo.
7.2
Como j descrito, um programa funcional consiste em uma expresso que avaliada para encontrar um valor que deve ser retornado e ligado a um identificador. Quando as operaes no
se tratarem de expresses, por exemplo, no caso de uma operao de I/O, que valor deve ser
retornado? Este retorno necessrio para que o paradigma seja respeitado. Caso contrrio, ele
corrompido.
A soluo adotada pelos idealizadores de Haskell foi introduzir um tipo especial chamado
ao". Quando o sistema detecta uma operao deste tipo, ele sabe que uma ao deve ser
executada. Existem aes primitivas, por exemplo escrever um caractere em um arquivo ou
receber um caractere do teclado, mas tambm aes compostas como imprimir vrias strings.
As operaes, em Haskell, cujos resultados de suas avaliaes sejam aes, so chamadas
de comandos", porque elas comandam o sistema para realizar alguma ao. As funes, cujos retornos sejam aes, tambm so chamadas de comandos. Os comandos realizam aes e
retornam um valor de um determinado tipo, que pode ser usado futuramente pelo programa.
Haskell prov o tipo IO a para permitir que um programa faa alguma operao de I/O e
retorne um valor do tipo a. Haskell tambm prov um tipo IO () que contm um nico elemento,
representado por (). Uma funo do tipo IO () representa uma operao de I/O (ao) que
retorna o valor (). Semanticamente, este o mesmo resultado de uma operao de I/O que no
retorna qualquer valor. Por exemplo, a operao de escrever a string Olha eu aqui!" pode ser
entendida desta forma, ou seja, um objeto do tipo IO ().
Existem muitas funes pr-definidas em Haskell para realizar aes, alm de um mecanismo
179
para seqencializ-las, permitindo que alguma ao do modelo imperativo seja realizada sem
ferir o modelo funcional.
7.2.1
Operaes de entrada
7.2.2
Operaes de sada
A operao de impresso de um texto, feita por uma funo que toma a string a ser escrita
como entrada, escreve esta string no dispositivo padro de sada e retorna um valor do tipo ().
Esta funo foi citada no Captulo 3, mas de forma genrica e sem nenhuma profundidade, uma
vez que, seria difcil o leitor entender sua utilizao com os conhecimentos sobre Haskell, at
aquele ponto, adquiridos. Esta funo putStr, pr-definida em Haskell, com o seguinte tipo:
putStr :: String -> IO ()
Agora podemos escrever "Olha eu aqui!", da seguinte forma:
aloGalvao :: IO ()
aloGalvao = putStr "Olha eu aqui!"
Usando putStr podemos definir uma funo que escreva uma linha de sada:
putStrLn :: String -> IO ()
putStrLn = putStr . (++ "\n")
cujo efeito adicionar o caractere de nova linha ao fim da entrada passada para putStr.
Para escrever valores em geral, Haskell prov a classe Show com a funo
show :: Show a => a -> String
que usada para transformar valores, de vrios tipos, em strings para que possam ser mostradas
atravs da funo putStr. Por exemplo, pode-se definir uma funo de impresso geral
print :: Show a => a -> IO ()
print = putStrLn . show
Se o objetivo for definir uma ao de I/O que no realize qualquer operao de I/O, mas que
retorne um valor, pode-se utilizar a funo
return :: a -> IO a
cujo efeito no realizar qualquer ao de I/O e retornar um valor do tipo a.
180
7.2.3
O comando do
O comando do um mecanismo flexvel, construdo para realizar duas operaes sobre aes em
Haskell:
1. a seqencializao de aes de I/O e
2. a captura de valores retornados por aes de I/O, para repass-los futuramente para outras
aes do programa.
Por exemplo, a funo putStrLn str, descrita anteriormente, pr-definida em Haskell e faz
parte do Prelude padro. Ela realiza duas aes. a primeira escrever a string str no dispositivo
padro de sada e a segunda fazer com que o prompt salte para a prxima linha. Esta operao
pode ser definida utilizando-se a notao do, da seguinte forma:
putStrLn :: String -> IO ()
putStrLn str = do putStr str
putStr "\n"
Neste caso, o efeito do comando do a seqencializao das aes de I/O, em uma nica
ao. A sintaxe do comando do regida pela regra do offside e pode-se tomar qualquer nmero
de argumentos (aes).
Como outro exemplo, pode-se querer escrever alguma coisa n vezes. Por exemplo, pode-se
querer executar 4 vezes a mesma operao do exemplo anterior. Uma primeira verso pode ser
o seguinte cdigo em Haskell:
faz4vezes :: String -> IO ()
faz4vezes str = do putStrLn str
putStrLn str
putStrLn str
putStrLn str
Apesar de funcionar corretamente, esta declarao mais parece com o mtodo da fora
bruta". Uma forma mais elegante de descrev-la transformar a quantidade de vezes que se
deseja que a ao seja executada em um parmetro.
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 recurso na cauda utilizada na definio da funo fazNvezes, simulando a instruo de controle while, to comum nas linguagens imperativas. Agora a funo
faz4vezes pode ser redefinida por
faz4vezes = fazNvezes 4
Apesar de terem sido mostrados apenas exemplos de sada, as entradas tambm podem ser
parte de um conjunto de aes seqencializadas. Por exemplo, pode-se querer ler duas linhas do
dispositivo de entrada padro e escrever a frase duas linhas lidas", ao final. Isto pode ser feito
da seguinte forma:
181
leia2linhas :: IO ()
leia2linhas = do getLine
getLine
putStrLn "duas linhas lidas"
Capturando valores lidos
No ltimo exemplo mostrado, foram lidas duas linhas mas nada foi feito com o resultado das
aes de getLine. Pode ser necessrio utilizar estes resultados no restante do programa. Isto
feito atravs da nomeao dos resultados das aes. Por exemplo,
getNput :: IO ()
getNput = do linha <- getLine
putStrLn linha
onde linha <- nomeia o resultado de getLine.
Apesar do identificador linha parecer com uma varivel em uma linguagem imperativa, seu
significado em Haskell bem diferente.
Exemplo. Seja a turma de uma disciplina escolar, onde os alunos tm 3 notas: n1, n2 e n3.
Vamos construir uma funo, em Haskell, que leia sequencialmente o nome de cada aluno e suas
3 notas e mostre o nome do aluno e sua mdia aritmtica. Este processo deve continuar at que
o nome do aluno seja Final.
Para isto, vamos construir a ao leiaAte da seguinte forma:
leiaAte :: IO ( )
leiaAte = do nome <- getline
if nome == "Final"
then return ()
else do (n1 <- getDouble
n2 <- getDouble
n3 <- getDouble
putStr (nome ++ show ((n1+n2+n3)/3.0) ++ "\n")
leiaAte)
A ao getDouble recebe um dado como entrada e interpreta o valor deste dado como um
tipo Double. Ela definida da seguinte forma:
getDouble :: IO Double
getDouble = do dado <- getLine
return (read dado :: Double)
A ao return ( ) representa uma ao que equivalente a fazer nada e retornar nada.
7.3
Os arquivos so considerados como variveis permanentes, cujos valores podem ser lidos ou
atualizados em momentos futuros, depois que o programa que os criou tenha terminada a sua
execuo. desnecessrio comentar a importncia que estas variveis tm para a computao.
No entanto, necessria uma forma de comunicao do programa com os arquivos. J foi vista
uma forma, atravs do comando do. A outra atravs de descritores.
182
Para obter um descritor (do tipo Handle) de um arquivo necessria a operao de abertura
deste arquivo para que futuras operaes de leitura e/ou escrita possam acontecer. Alm disso,
necessria uma operao de fechamento deste arquivo, aps suas operaes terem sido realizadas,
para que os dados no sejam perdidos quando o programa terminar sua execuo.
A Tabela 7.1 mpstra as formas como os arquivos podem ser abertos, operados e fechados em
Haskell, usando o comando openFile.
Tabela 7.1: Forma de operaes com
IOMode
l
escreve Posio inicial
ReadMode
Sim No
Incio do arquivo
WriteMode
No Sim
Incio do arquivo
ReadWriteMode Sim Sim
Incio do arquivo
AppendMode
No
Sim
Fim do arquivo
arquivos em Haskell.
Notas
O arquivo deve existir
Se existir o arquivo perde os dados
O arquivo criado se no existir
seno os dados so conservados
O arquivo criado se no existir e
os dados existentes so conservados
Os programas em Haskell podem trabalhar com arquivos tipo texto ou com arquivos binrios.
Neste ltimo caso, necessrio utilizar openBinaryFile em vez de openFile. O sistema operacional Windows processa arquivos textos de forma diferente como processa arquivos binrios. O
sistema operacional Linux utiliza tanto openFile quanto openBinaryFile, mas aconselhvel
usar openBinaryFile para processar arquivos binrios para efeito de portabilidade.
Estas operaes so descritas em Haskell, da seguinte forma:
data IOMode = ReadMode | WriteMode | AppendMode | ReadWriteMode
openFile :: FilePath -> IOMode -> IO Handle
hClose :: Handle -> IO ()
Por conveno, todos os comandos usados para tratar descritores de arquivos so iniciadas
com a letra h. Por exemplo, as funes
hPutChar :: Handle -> Char -> IO ()
hPutStr :: Handle -> String -> IO ()
hPutStrLn :: Handle -> String -> IO ()
hPrint :: Show a => Handle -> a -> IO ()
so utilizadas para escrever alguma coisa em um arquivo. J os comandos
hGetChar :: Handle -> IO ()
hGetLine :: Handle -> IO ()
so utilizados nas operaes de leituras em arquivos. As funes hPutStrLn e hPrint incluem
um caractere \n para a mudana de linha ao final da string.
Haskell tambm permite que todo o contedo de um arquivo seja retornado como uma nica
string, atravs da funo
hGetContents :: Handle -> String
No entanto, h que se fazer uma observao. Apesar de parecer que hGetContents retorna
todo o contedo de um arquivo de uma nica vez, no realmente isto o que acontece. Na
realidade, a funo retorna uma lista de caracteres, como esperado, mas de forma lazy, onde os
elementos so lidos sob demanda.
183
7.3.1
Lembremos que um arquivo tambm 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 ()
Tambm podemos acrescentar uma string ao final de um arquivo com o comando
appendFile :: FilePath -> String -> IO ()
Ento, qual a necessidade de se utilizar descritores? A resposta imediata: eficincia. Vamos
analisar.
Toda vez que o comando writeFile ou appendFile executado, deve acontecer tambm
uma seqncia de aes, ou seja, o arquivo deve ser inicialmente aberto, a string deve ser escrita
e, finalmente, o arquivo deve ser fechado. Se muitas operaes de escrita forem necessrias, ento
sero necessrias muitas destas seqncias. Ao se utilizar descritores, necessita-se apenas de uma
operao de abertura no incio e outra de fechamento no final.
Para ler ou escrever a partir de um descritor, que normalmente corresponde a um arquivo
em disco, o sistema operacional mantm um registro interno da posio atual de leitura do
arquivo, ou seja, um ponteiro para a posio atual. A cada vez que uma leitura feita, o sistema
operacional retorna o valor do dado apontado por este ponteiro e incrementa o ponteiro para
apontar para o prximo dado a ser lido.
O comando hTell pode ser utilizado para ver a posio corrente deste ponteiro em relao
ao incio do arquivo, que tem a posio 0 (zero). Isto significa que, quando um arquivo criado,
ele criado vazio e o ponteiro tem o valor 0 (zero). Quando se escrevem 10 bytes no arquivo, a
posio ser 10. hTell tem o tipo hTell :: Handle > IO Integer.
O complemento de hTell hSeek que permite trocar o valor da posio atual. Ele toma trs
parmetros: Handle, SeekMode e um endereo. O parmetro SeekMode pode ter trs valores
distintos para especificar como uma determinada posio deve ser interpretada. O primeiro valor
]textbfAbsoluteSeek indicando que a posio deve ser a atual que tambm a indicada pelo
valor de hTell. O segundo valor RelativeSeek que deve ser interpretado como um valor a
partir da posio atual, onde um valor positivo indica uma posio para a frente da posio
atual, um valor negativo indica uma posio para atrs da posio atual no arquivo. O terceiro
valor SeekFromEnd que especifica a quantidade de bytes antes do final do arquivo.
Vamos mostrar um exemplo baseado em OSullivan [29] que apresentado em vrias verses
para se verificar as possibilidades que Haskell oferece, atravs das aes.
import System.IO
import Data.Char(toUpper)
main :: IO ()
main = do entrada <- openFile "entra.txt" ReadMode
saida <- openFile "sai.txt" WriteMode
loop_principal entrada saida
hClose entrada
hClose saida
loop_principal :: Handle -> Handle -> IO ()
184
A partir destes exemplos, pode-se observar que o comando hGetContents utilizado como
um filtro, onde os dados so lidos em um arquivo, processados e escritos em um outro arquivo.
Este tipo de processamento bastante comum e, por este motivo, foram criadas duas funes para
atender a este demanda: readFile e writeFile. Estas duas funes gerenciam todos os detalhes
de abertura, leitura, processamento e fechamento de arquivos como strings. A funo readFile
usa hGetContents internamente. Desta forma, o exemplo anterior pode ser construdo ainda
mais sinteticamente.
import Data.Char(toUpper)
main :: IO ()
main = do inpStr <- readFile "entra.txt"
writeFile "saida.txt" (map toUpper inpStr)
7.3.2
Canais
Os descritores tambm podem ser associados a canais, que so portas de comunicao no associadas diretamente a um arquivo. Os canais mais comuns so: a entrada padro (stdin), a rea
de sada padro (stdout) e a rea de erro padro (stderr). As operaes de I/O para caracteres
e strings em canais incluem as mesmas listadas anteriormente para a manipulao de arquivos.
Na realidade, as funes getChar e putChar so definidas como:
getChar = hGetChar stdin
putChar = hputChar stdout
At mesmo hGetContents pode ser usada com canais. Neste caso, o fim de um canal
sinalizado com um cartactere de fim de canal que, na maioria dos sistemas, Ctrl-d.
Normalmente, um descritor um arquivo, mas pode ser tambm uma conexo de rede, um
drive ou um terminal. Para verificar se um dado descritor pode, ou no, ser varrido, pode-se
usar o comando hIsSeekable que retorna um valor booleano.
7.4
Gerenciamento de excees
Vamos agora nos reportar a erros que podem acontecer durante as operaes de I/O. Por exemplo,
pode-se tentar abrir um aquivo que ainda no existe, ou pode-se tentar ler um caractere de um
arquivo que j atingiu seu final. Certamente, no se deve querer que o programa pre por
estes motivos. O que normalmente se espera, que o erro seja reportado como uma condio
anmala, mas que possa ser corrigida, sem a necessidade de que o programa seja abortado. Para
fazer este gerenciamento de excees, em Haskell, so necessrios apenas alguns comandos de
IO.
As excees tm o tipo IOError. Entre as operaes permitidas sobre este tipo, est uma
coleo de predicados que podem ser usados para testar tipos particulares de excees. Por
exemplo,
isEOFError :: IOError -> Bool
detecta o fim de um arquivo.
Mesmo assim, existe uma funo catch que faz o gerenciamento de excees. Seu primeiro
argumento a ao de I/O que se est tentando executar e seu segundo argumento um descritor
de excees, do tipo IOError > IO a. Vejamos:
186
-187
menu :: IO ( )
menu = do putStrLn " "
putStrLn " ---------------------------------------"
putStrLn "|
|"
putStrLn "|
CADASTRO DE PESSOAS
|"
putStrLn "|
|"
putStrLn "|
a - Insere cadastro
|"
putStrLn "|
b - Imprime cadastro
|"
putStrLn "|
c - Busca por nomes
|"
putStrLn "|
d - Soma das idades
|"
putStrLn "|
e - Mdia das alturas
|"
putStrLn "|
f - Busca por sexo
|"
putStrLn "|
g - Excluir um cadastro
|"
putStrLn "|
h - Excluir todos os cadastros
|"
putStrLn "|
i - Sair do sistema
|"
putStrLn "|_______________________________________|"
putStr "Digite uma das opes: "
le_opcao
le_opcao :: IO ( )
le_opcao = do opcao <- getChar
putStr "\n"
f_menu (toUpper opcao)
f_menu :: Char -> IO ( )
f_menu i = do case i of
A -> insere_cadastro
B -> imprime_cadastros
C -> busca_p_nomes
D -> soma_d_idades
E -> media_d_alturas
F -> busca_p_sexo
G -> excluir_um_cadastro
H -> excluir_todos_cadastros
otherwise -> sair i
putStrLn "Operao concluda"
if not(i==I) then menu else putStr " "
insere_cadastro :: IO ( )
insere_cadastro =
do putStrLn "Nome: "
nm <- getLine
putStrLn "Idade: "
id <- getLine
putStrLn "Altura: "
alt <- getLine
putStrLn "M - Masculino | F - Feminino"
putStrLn "Sexo: "
sex <- getChar
let cadastro = nm++"#"++id++"#"++alt++"#"++[(toUpper sex)]++""
pt_arq <- abreArquivo "dados.txt" AppendMode
188
189
190
nome (a:b:c:d:[ ]) = a
idade (a:b:c:d:[ ]) = b
altura (a:b:c:d:[ ]) = c
sexo (a:b:c:d:[ ]) = d
copiar origem destino =
do pt_arq <-abreArquivo origem ReadMode
conteudo <- (hGetContents pt_arq)
aux_pt_arq <- abreArquivo destino WriteMode
hPutStr aux_pt_arq conteudo
fechaArquivo aux_pt_arq
fechaArquivo pt_arq
-- FUNES AUXILIARES DE BUSCA (176)
busca_p_algo :: ([[String]] -> a -> IO b) -> a -> IO ( )
busca_p_algo funcao filtro =
do putStrLn " "
putStrLn "-----------------------------------------"
pt_arq <- abreArquivo "dados.txt" ReadMode
conteudo <- (hGetContents pt_arq)
cadastros <- (converteConteudo (conteudo))
funcao cadastros filtro
fechaArquivo pt_arq
putStrLn "-----------------------------------------"
busca_por_nome
busca_por_nome
busca_por_nome
|(nome x)
busca_por_sexo
busca_por_sexo
busca_por_sexo
| (sexo x)
191
"++b) x)
"++b) x)
apaga_p_nome [ ] nm = "\n"
apaga_p_nome (x : xs) nm
|nm == (nome x) = (apaga_p_nome xs nm)
|otherwise
= (foldl1 (\a b->a++"#"++b) x) ++ "\n" ++ (apaga_p_nome xs nm)
--FUNES AUXILIARES DE ARQUIVOS
--modo: AppenMode, WriteMode, ReadMode
abreArquivo :: String -> IOMode -> IO Handle
abreArquivo arquivo modo =
catch (openFile arquivo modo)
(\_ -> do {
putStrLn ("Impossvel abrir "++ arquivo);
putStrLn "Ser aberto com um nome defaul: dados.txt e limpo";
pt_arq <- abreArquivo "dados.txt" WriteMode;
fechaArquivo pt_arq;
abreArquivo "dados.txt" ReadMode
}
)
fechaArquivo :: Handle -> IO ( )
fechaArquivo handle_arq = hClose handle_arq
7.5
Resumo
Este foi o Captulo final deste trabalho, dedicado semntica de aes, adotadas em Haskell,
para tratar operaes de entrada e sada, representando a comunicao que o programa deve
ter com perifricos e com os arquivos. Na realidade, ela implementada em Haskell atravs de
Mnadas, uma teoria matemtica bastante complexa e que, por este motivo, est 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 comunicao com o mundo
externo ao programa. Isto inclui a leitura e escrita de dados em arquivos, bem como a criao e
o fechamento de arquivos, armazenando dados para serem utilizados futuramente.
Este estudo foi baseado nos livros de Simon Thompson [46], de Richard Bird [4] e de Paul
Hudak [11], como tambm nos excelentes textos de Philip Wadler [48, 49]. Este tema ainda
considerado novo e complexo pelos pesquisadores das linguagens funcionais e acreditamos ser o
motivo pelo qual ele ainda muito pouco tratado na literatura.
Com este Captulo, esperamos ter cumprido nosso objetivo que foi o de proporcionar aos
estudantes iniciantes das linguagens funcionais, alguma fundamentao e conceitos alm de desenvolver algumas aplicaes utilizando a linguagem de programao Haskell.
192
Bibliografia
[1] AHO, Alfred V; SETHI, Ravi et ULLMAN, Jeffrey D. Compilers, Principles, Techniques,
and Tools. 2nd. Edition. Addison-Wesley Publishing Company; 1988.
[2] ANDRADE, Carlos Anreazza Rego. AspectH: Uma Extenso Orientada a Aspectos de
Haskell. Dissertao de Mestrado. Centro de Informtica. UFPE. Recife, Fevereiro 2005.
[3] BARENDREGT, H. P. 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] BOYER, Carl B. Histria da Matemtica. Traduo de Elza Gomide. Editora Edgard
Blcher Ltda. So Paulo, 1974.
[6] BRAINERD, W. S. et LANDWEBER, L. H. Theory of Computation. John Wiley & Sons,
1974.
[7] CURRY, H. B. et FEYS, R. and CRAIG, W. Combinatory Logic. Volume I; North Holland,
1958.
[8] DAVIE, Antony J. T. An Introduction to Functional Programming Systems Using Haskell.
Cambridge Computer Science Texts. Cambridge University Press. 1999.
[9] DAVIS, Philip J. et HERSH, Reuben. A Experincia Matemtica. Traduo de Joo Bosco
Pitombeira. Editora Francisco Alves. Rio de Janeiro, 1985.
[10] HINDLAY, J. Roger. Basic Simple Type Theory. Cambridge Tracts in Theorical Computer
Science, 42. Cambridge University Press. 1997.
[11] HUDAK, Paul. The Haskell School of Expression: Learning Funciotonal Programming
Through Multimedia. Cambridge University Press, 2000.
[12] HUDAK, Paul et all. A History of Haskell: being lazy with class. January 2007.
[13] HUGHES, John. Why Functional Programming Matters. In Turner D. T. Ed. Research
Topics in Funcitonal Programming. Addison-Wesley, 1990.
[14] HUGHES,
John.
The
Design
of
http://citeseer.ist.psu.edu/hughes95design.html. 1995.
Pretty-printing
Library.
[30] PAULSON, Laurence C. ML for the Working Programmer. Cambridge University Press,
1991.
[31] PEMMARAJU, Sriram et SKIENA, Steven. Computational Discrete Mathematics: Combinatorics and Graph Theory with Mathematica. Cambridge University Press. 2003.
[32] PEYTON JONES, S. L. The Implementation of Functional Programming Languages. C.
A. R. Hoare Series Editor. Prentice/Hall International. 1987.
[33] PEYTON JONES, S. L.; GORDON, A. and FINNE, S. Concurrent Haskell. In 23rd
ACM Symposium on Principles of Programming Languages (POPL96), pages 295-308. St
Petersburg Beach, Florida. ACMPress. 1996.
[34] RABHI, Fethi et LAPALME, Guy. Algorithms: A Functional Programming Approach.
2nd. edition. Addison-Wesley. 1999.
[35] ROJEMO, Niklaus. Garbage Collection and Memory Efficiency in Lazy Functional Languages. Ph.D Thesis. Department of Computing Science, Chalmers University. 1995.
194
[36] S, Claudio Cesar. Haskell: uma abordagem prtica. Novatec Editora Ltda. So Paulo,
2006.
[37] SCHMIDT, D. A. Denotational Semantics. Allyn and Bacon, Inc. Massachusetts, 1986.
[38] SCOTT, D. Data Types as Lattices. SIAM Journal of Computing. Vol. 5,3. 1976.
[39] SKIENA, Steven S. et REVILLA, Miguel A. Programming Challenges: The Programming
Context Training Manual. Texts in Computer Science. Springer Science+Business Media,
Inc. 2003.
[40] SOUZA, Francisco Vieira de. Gerenciamento de Memria em CMC. Dissertao de
Mestrado. CIn-UFPE. Maro de 1994.
[41] SOUZA, Francisco Vieira de. Teoria das Categorias: A Linguagem da Computao. Exame
de Qualificao. Centro de Informtica. UFPE. 1996.
[42] SOUZA, Francisco Vieira de et LINS, Rafael Dueire. Aspectos do Comportamento Espaotemporal de Programas Funcionais em Uni e Multiprocessadores. X Simpsio Brasileiro de
Arquitetura de Computadores e Processamento de Alto Desempenho. Bzios-RJ. Setembro.
1998.
[43] SOUZA, Francisco Vieira de et LINS, Rafael Dueire. Analysing Space Behaviour of Functional Programs. Conferncia Latino-americana de Programao Funcional. Recife-Pe.
Maro. 1999.
[44] SOUZA, Francisco Vieira de. Aspectos de Eficincia em Algoritmos para o Gerenciamento
Automtico Dinmico de Memria. Tese de Doutorado. Centro de Informtica-UFPE.
Recife. Novembro. 2000.
[45] STOY, J. E. Denotational Semantics: The Scott-Strachey Approach to Programming Language Theory. MIT Press, 1977.
[46] THOMPSON, Simon. Haskell: The Craft of Functional Programming. 2nd. Edition. Addison Wesley. 1999.
[47] TURNER, David A. A New Implementation Technique for Applicative Languages. Software
Practice and Experience. Vol. 9. 1979.
[48] WADLER, Philip. Comprehendig Monads. Mathematical Structures in Computer Science;
2:461-493, 1992.
[49] WADLER, Philip. The Essence of Functional Programming. In 20th ACM Symposium on
Principles of Programming Languages (POPL92), pages 1-14. ACM. Albuquerque. 1992.
[50] WADLER, Philip. Why no ones Uses Functional Languages. Functional Programming.
ACM SIGPLAN. 2004.
[51] WELCH, Peter H. The -Calculus. Course notes, The University of Kent at Canterbury,
1982.
195
196
Apndice A
-> t
error "(!!): index too large"
x
xs !! n
197
12. dropWhile. Remove o segmento inicial de uma lista se os elementos do segmento satisfizerem uma determinada propriedade:
dropWhile :: (t -> Bool) -> [t] -> [t]
dropWhile p [ ]
= [ ]
dropWhile p (x : xs) = if p x then dropWhile p xs else x : xs
13. filter. Seleciona os elementos de uma lista que apresentam uma determinada propriedade:
filter :: (t -> Bool) -> [t] -> [t]
filter p [ ]
= [ ]
filter p (x : xs) = if p x then x : filter p xs else filter p xs
14. flip. Inverte o posicionamento dos argumentos de uma funo:
flip :: (u -> t -> v) -> t -> u -> v
flip f x y = f y x
198
15. foldl. Aplica uma funo entre os elementos de uma lista pela esquerda:
foldl :: (u -> t -> u) -> u -> [t] -> u
foldl f e [ ]
= e
foldl f e (x : xs) = strict (foldl f) (f e x) xs
16. foldl1. Aplica uma funo entre elementos de listas no vazias, pela esquerda:
foldl1 :: (t -> t -> t) -> [t] -> t
foldl1 f [ ]
= error "foldl1 : empty list"
foldl1 f (x : xs) = foldl f x xs
17. foldr. Aplica uma funo entre os elementos de uma lista pela direita:
foldr :: (t -> u -> u) -> u -> [t] -> u
foldr f e [ ]
= e
foldr f e (x : xs) = f x (foldr f e xs)
18. foldr1. Aplica uma funo entre elementos de listas no vazias, pela direita:
foldr1
foldr1
foldr1
foldr1
:: (t -> t
f [ ]
f [x]
f (x : xs)
23. iterate. Produz uma lista infinita de aplicaes iteradas de uma funo:
199
:: [t] -> t
[ ]
= error "last: empty list"
[x]
= x
(x : y : xs) = last (y : xs)
34. scanl1. Aplica foldl1 a todo o segmento inicial no vazio de uma lista no vazia:
scanl1 :: (t -> t -> t) -> [t] -> [t]
scanl1 f [ ]
= error "scanl1: empty list"
scanl1 f (x : xs) = scanl f x xs
35. scanr. Aplica foldr ao segmento cauda de uma lista:
scanr :: (t -> u -> u) -> u -> [t] -> [u]
scanr f e [ ]
= [e]
scanr f e (x : xs) = f x (head ys) : ys
where ys = scanr f e xs
36. scanr1. Aplica foldr1 a cauda no vazia de uma lista:
scanr1
scanr1
scanr1
scanr1
:: (t -> t -> t)
f [ ]
=
f [x]
=
f (x : y : xs) =
([t], [t])
= ([ ], xs)
= ([ ], [ ])
= (x : ys, zs)
where (ys, zs) = splitAt n xs
201
[t]
= [ ]
= [ ]
= x : take n xs
43. takeWhile. Seleciona o segmento inicial dos elementos de uma lista que satisfazem um
dado predicado:
takeWhile :: (t -> Bool) -> [t] -> [t]
takeWhile p [ ]
= [ ]
takeWhile p (x : xs) = if p x then x : takeWhile p xs else [ ]
44. uncurry. Converte uma funo currificada em uma verso no currificada:
uncurry :: (t -> u -> v) -> (t, u) -> v
uncurry f xy = f (fst xy) (snd xy)
45. until. Aplicada a um predicado, uma funo e um valor, retorna o resultado da aplicao
da funo ao valor, o menor nmero de vezes para satisfazer o predicado:
until :: (t -> Bool) -> (t -> t) -> t -> t
until p f x = if p x then x else until p f (f x)
46. unzip. Transforma uma lista de pares em um par de listas:
unzip :: [(t, u)] -> ([t], [u])
unzip = foldr f ([ ], [ ])
where f (x, y) = cross ((x : ), (y : ))
47. wrap. Converte um valor em uma lista de um nico elemento:
wrap :: t -> [t]
wrap x = [x]
48. zip. Transforma um par de listas em uma lista de pares:
202
zip
zip
zip
zip
203
204
Apndice B
Introduo
Este Apndice se fez necessrio para mostrar ao usurio como ele pode construir programas
em Haskell e compil-los utilizando GHC (Glasgow Haskell Compiler ). Os programas executveis permitem a utilizao de ferramentas importantes na depurao e otimizao de programas. Por exemplo, a execuo de um programa pode ser feita com o auxlio do RTS (Run
Time System) que permite que o usurio escolha o coletor de lixo a ser utilizado na execuo
de seu programa, podendo ser o coletor de cpia ou o coletor geracional, o tamanho do heap
de execuo, alm de uma srie de alternativas de profile. Se o usurio assim o desejar, GHC
pode reportar a quantidade de chamadas feitas ao coletor de lixo, mostrando todas as clulas
recicladas e os tempos de CPU, de sistema e total, gastos em cada chamada. Outra ferramenta
de profile importante disponvel em GHC se refere criao de estatsticas sobre as chamadas
a cada funo e os tempos de durao delas que podem ser mostradas de forma grfica, podendo
serem teis na otimizao de cada uma destas funes. Outra informao importante se refere a
quantidade cache miss e pagefaults.
B.2
O compilador GHC pode ser instalado em vrias plataformas mas, neste Apndice, ser analisada a sua instalao sob o sistema operacional Windows. Os pacotes binrios de GHC devem
funcionar sob Windows Vista e XP, mesmo Windows 2000.
O primeiro passo para quem deseja instalar GHC visitar a pgina http://www.haskell.org/
e seguir as instrues ali descritas para download e instalao. O instalador tem o nome de ghc6.10.3-i386-windows.exe (50MB) que contempla tambm o suporte para compilao de arquivos
C++. Aps realizar o download, d um clique duplo para iniciar o processo de instalao que
envolve alguns passos descritos pelo instalador. Ao trmino do processo de instalao, o submenu
All Programs deve conter o folder GHC, dentro do qual deve existir um cone que pode ser
usado para executar o ghci.
205
B.3
Compilando em GHC
Para a gerao do um arquivo executvel a partir de um programa com diversos mdulos, deve
existir um mdulo Main que ser parte do arquivo Main.hs e deve conter obrigatoriamente a
funo main(). Para compilar um programa standalone, mesmo assim, necessria a existncia da funo main() da mesma forma. Por exemplo, para compilar um programa simples
primeiro.hs, cujo contedo o seguinte,
main = putStr "Primeiro programa executavel em Haskell"
deve-se escrever este cdigo e salvar o arquivo com o nome primeiro.hs e fazer o seguinte
comando:
ghc -c primeiro.hs
A diretiva de compilao -c informa ao ghc que ele deve gerar apenas cdigo objeto, e
no gerar o programa executvel ainda. Neste caso, os programas primeiro.o e primeiro.hi
sero criados no diretrio que consta o arquivo primeiro.hs, caso nenhum erro seja detectado
no programa. Para gerar o arquivo executvel, primeiro.exe sob a plataforma Windows ou
primeiro sob o sistema operacional Unix ou seus clones necessrio chamar novamente o ghc
da seguinte forma:
ghc -o primeiro primeiro.o
O leitor pode verificar que o programa primeiro.exe (em Windows) se encontra do diretrio
em uso e cham-lo diretamente da linha de comando.
B.3.1
Em algumas aplicaes, pode ser extremamente til receber parmetros da linha de comandos.
Em Haskell, isto possvel se for importado o mdulo System.Environment da biblioteca de
Haskell para o script do programa a ser compilado. Utilizando o mesmo arquivo anterior, pode-se
fazer isso da seguinte forma:
import System.Environment
main :: IO ()
main = do
args <- getArgs
putStr (args!!0)
Ao compilar este script em GHC, o programa ir escrever na tela o que for escrito na linha
de comandos, ou seja, a frase que ser passada como argumento atravs da linha de comandos.
O leitor pode verificar que a sintaxe de Haskell para a passagem de parmetros semelhante a
de C, com os argumentos armazenados em uma lista, em vez de em um array como em C. No
caso em voga, os demais parmetros podem ser acessados apenas mudando o valor do elemento
da lista args.
B.3.2
Diretivas de compilao
J foi mencionado que GHC oferece uma diversidade de diretivas de compilao, atravs de seus
flags. Algumas diretivas so colocadas para facilitar a otimizao do programa e outras so
206
207