Académique Documents
Professionnel Documents
Culture Documents
PROGRAMAC
AO
USANDO HASKELL
Programa
c
ao Funcional
Usando Haskell
UFPI/CCN/DIE
Teresina-Pi
Agosto de 2005
c
Copyright 2005,
Departamento de Inform
atica e Estatstica,
Centro de Ciencias da Natureza,
Universidade Federal do Piau.
Todos os direitos reservados.
A reproducao do todo ou parte deste
trabalho somente sera permitida para ns
educacionais e de pesquisa e com a expressa
autorizacao do autor.
c
Copyright 2005,
Departamento de Inform
atica e Estatstica,
Centro de Ciencias da Natureza,
Universidade Federal do Piau.
All rights reserved.
Reproduction of all or part of this work only
will be permitted for educational or research use
and with expressed permission of the author.
iii
Apresentac
ao
Esta Apostila representa a compilac
ao de v
arios t
opicos, desenvolvidos por pesquisadores de
renome, no campo da programac
ao funcional. Ela tem como objetivo servir de guia aos prossionais de Inform
atica que desejam ter um conhecimento inicial sobre o paradigma funcional de
forma geral e, em particular, sobre programac
ao usando Haskell. Ultimamente, Haskell tem se
tornado a linguagem funcional padr
ao do discurso, j
a existindo v
arios interpretadores e compiladores para ela, alem de v
arias ferramentas de an
alise de programas nela codicados (proles).
Para atingir este objetivo, acreditamos que o estudo deva ser acompanhado de algum conhecimento, mesmo que mnimo, sobre a fundamentac
ao destas linguagens e da forma como
elas s
ao implementadas. Este conhecimento proporciona ao leitor uma vis
ao das principais caractersticas e propriedades destas linguagens. Em particular, e importante entender porque as
tecnicas utilizadas na compilac
ao das linguagens imperativas n
ao se mostraram adequadas na
compilac
ao de linguagens funcionais.
Em 1978, John Backus advogou o paradigma funcional como o que oferecia a melhor soluc
ao
para a chamada crise do software. As linguagens funcionais s
ao apenas uma sintaxe mais
c
omoda para o -c
alculo. David Turner [36] mostrou, em 1979, que a l
ogica combinatorial
poderia ser extendida de forma a possibilitar a implementac
ao eciente de linguagens funcionais.
Esse trabalho provocou uma corrida em direc
ao `
a pesquisa nesta
area, gerando uma variedade
de tecnicas de implementac
ao destas linguagens.
Dentre estas tecnicas, uma que tem sido adotada, com resultados promissores, e a utilizac
ao
do -c
alculo como linguagem intermedi
aria entre a linguagem de alto nvel e a linguagem de
m
aquina. Os programas codicados em alguma linguagem funcional de alto nvel s
ao traduzidos
para programas em -c
alculo e destes para programas em linguagem de m
aquina. Neste caso, o
-c
alculo desempenha um papel semelhante ao que a linguagem Assembly exerce, como linguagem
de montagem, na compilac
ao de linguagens imperativas. Esta metodologia tem dado certo, uma
vez que j
a se conhecem tecnicas ecientes de traduc
ao de programas em -c
alculo para programas
execut
aveis, faltando apenas uma traduca
o eciente de programas codicados em uma linguagem
funcional de alto nvel para programas em -c
alculo.
Esta Apostila tem incio em seu primeiro Captulo tratando das caractersticas das linguagens funcionais, destacando suas vantagens em relac
ao `
as linguagens de outros paradigmas que
utilizam atribuic
oes destrutivas. Em seguida, e feita uma introduc
ao ao -c
alculo. Apesar do
car
ater introdut
orio, achamos ser suciente para quem quer dar os primeiros passos em direc
ao
a aprendizagem desta tecnica. Os Captulos subseq
`
uentes se referem todos a
` Programac
ao Funcional usando Haskell.
Por ser uma primeira tentativa, a Apostila contem erros e sua apresentac
ao did
atico-pedag
ogica deve ser revista. Neste sentido, agradecemos crticas construtivas que ser
ao objeto de
an
alise e reex
ao e, por isto mesmo, muito bem-vindas.
Teresina-Pi, agosto de 2005.
Francisco Vieira de Souza
iv
Conte
udo
Introdu
c
ao
viii
1 Programa
c
ao Funcional
11
1.1
Introducao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
11
1.2
Hist
orico . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
12
1.3
13
1.4
14
1.5
Transparencia referencial . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
16
1.6
Interfaces manifestas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
18
1.7
19
1.8
Denicao de funcoes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
20
1.9
Resumo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
25
2 -c
alculo
27
2.1
Introducao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
27
2.2
-expressoes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
28
2.3
A sintaxe do -calculo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
29
2.3.1
29
2.4
30
2.5
-abstracoes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
30
2.6
31
2.6.1
33
2.7
Combinadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
33
2.8
35
2.8.1
-conversao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
35
2.8.2
-conversao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
35
2.8.3
-conversao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
35
2.8.4
Nomeacao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
36
2.8.5
Captura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
36
37
38
2.9
38
39
40
2.14 Resumo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
42
3 Programa
c
ao funcional em Haskell
43
3.1
Introducao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
43
3.2
Primeiros passos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
44
3.2.1
O interpretador Hugs . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
45
3.2.2
Identicadores em Haskell . . . . . . . . . . . . . . . . . . . . . . . . . . .
47
Funcoes em Haskell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
47
3.3.1
Construindo funcoes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
49
3.3.2
51
3.3.3
51
52
3.4.1
52
3.4.2
Programando com n
umeros e strings . . . . . . . . . . . . . . . . . . . . .
59
3.4.3
60
3.4.4
Escopo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
61
3.4.5
C
alculos: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
62
3.4.6
Projeto de programas . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
65
3.4.7
Provas de programas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
65
Resumo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
69
3.3
3.4
3.5
4 O tipo Lista
4.1
71
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
72
4.1.1
. . . . . . . . . . . . . . . . . . . . . . . .
72
4.1.2
73
4.2
75
4.3
78
4.4
84
4.4.1
A funcao map . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
85
4.4.2
Funcoes anonimas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
88
Polimorsmo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
90
4.5.1
Tipos vari
aveis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
91
4.5.2
91
4.6
Inducao estrutural . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
92
4.7
Composicao de funcoes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
95
4.7.1
96
4.5
Composicao avancada . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
vi
4.7.2
4.8
4.9
96
Aplicacao parcial . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
98
4.8.1
Secao de operadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
99
4.8.2
Curricacao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
99
4.9.2
109
5.1
Introducao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
5.2
5.3
5.4
5.2.1
5.2.2
5.2.3
Assinaturas e inst
ancias . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
5.2.4
5.2.5
Tipos algebricos
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
5.3.1
5.3.2
5.3.3
Derivando inst
ancias de classes . . . . . . . . . . . . . . . . . . . . . . . . 118
5.3.4
5.3.5
Recursao m
utua . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120
5.3.6
5.4.2
5.5
5.6
M
odulos em Haskell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
5.7
5.6.1
5.6.2
5.6.3
O m
odulo main . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
5.6.4
5.6.5
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
5.7.1
5.7.2
5.7.3
5.7.4
5.8
5.9
Express
oes ZF (revisadas) . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
5.8.2
5.8.3
Resumo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
6 Programa
c
ao com a
c
oes em Haskell
145
6.1
Introducao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
6.2
6.3
6.2.1
6.2.2
6.2.3
A notacao do . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
6.3.2
Canais . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150
6.4
6.5
Resumo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151
Refer
encias Bibliogr
aficas
152
viii
Introdu
c
ao
Functional languages privide a framework
in which the crucial ideas of modern programming
are presented in the clearest prossible way.
(Simon Thompson in [35])
Em 2004, Philip Wadler1 escreveu o artigo Why no one uses functional languages, onde ele
comenta sobre a pouca utilizacao das linguagens funcionais na Ind
ustria e ambientes comerciais
[37]. Para ele, dizer que ninguem usa linguagem funcional, e um exagero. As chamadas telef
onicas no Parlamento Europeu s
ao roteadas por um programa escrito em Erlang, a linguagem
funcional da Ericsson. A rede Cornell distribui CDs virtuais usando o sistema Esemble, escrito
em CAML e a Polygram vende CDs na Europa usando Natural Expert, da Software AG. As
linguagens Erlang (www.erlang.se) e ML Works de Harlequin (www.harlequin.com) apresentam
um extensivo ambiente de suporte ao usu
ario. Alem disso, as linguagens funcionais s
ao mais
adequadas a` construcao de provadores de teoremas, incluindo o sistema HOL que foi utilizado
na depuracao do projeto de multiprocessadores da linha HP 9000.
Ainda segundo Wadler, as linguagens funcionais produzem um c
odigo de maquina com uma
melhoria de uma ordem de magnitude e, nem sempre, estes resultados s
ao mostrados. Normalmente, se mostram fatores de 4. Mesmo assim, um c
odigo que e quatro vezes menor, quatro vezes
mais rapido de ser escrito ou quatro vezes mais facil de ser mantido n
ao pode ser jogado fora.
Para ele, os principais fatores que inuenciam na escolha de uma linguagem de programacao
sao:
Compatibilidade. Os sistemas n
ao sao mais construdos monoliticamente, como eram
no passado. Atualmente, eles s
ao escritos de forma modular. Os modulos sao construdos
por usu
arios em tempos e locais possivelmente diferentes e sao ligados atraves de interfaces
bem denidas. Muitas linguagens funcionais j
a apresentam facilidades para a construcao
de grandes softwares com formas bem adequadas de construcao de modulos e algumas
necessario acabar com o
linguagens funcionais ainda n
ao oferecem estas facilidades. E
isolamento das linguagens funcionais e incorporar a elas facilidades para a comunicacao
entre programas funcionais e programas codicados em outras linguagens pertencentes
a outros paradigmas. A Ind
ustria da Computacao esta comecando a distribuir padr
oes
como CORBA e COM para suportar a construcao de software a partir de componentes
reutiliz
aveis. Atualmente, os programas em Haskell ja podem ser empacotados como um
componente COM e qualquer componente COM pode ser chamado a partir de Haskell.
Entre outras aplicacoes, isto permitiu que Haskell fosse utilizada como linguagem para a
construcao do Internet Explorer, o browser da Microsoft.
Bibliotecas. Muitos usu
arios escolheram Tcl, atrados, principalmente, pela biblioteca
gr
aca Tk. Muito pouco da atratividade de Java tem a ver com a linguagem em si,
1
Philip Wadler trabalha nos grupos de ML e de Unix na Bell Labs. Ele e co-autor das linguagens Haskell e
GJ. Alem de v
arios artigos publicados, ele tambem e co-editor da revista Journal of Functional Programming.
A sem
antica de aco
es e um tema tratado no Captulo 6. Os m
onadas n
ao s
ao aqui tratados, uma vez que seu
estudo requer, como pre-requsito, o conhecimento aprofundado sobre implementaca
o de linguagens funcionais.
Princpios da programa
c
ao estruturada
Hoare enumerou seis princpios fundamentais da estruturacao de programas [24]:
1. Transpar
encia de significado. Este princpio arma que o signicado de uma express
ao,
como um todo, pode ser entendido em termos dos signicados de suas sub-express
oes.
Assim, o signicado da express
ao E + F depende simplesmente dos signicados das subexpressoes E e F, independente das complicacoes de cada uma delas.
2. Transpar
encia de prop
ositos. Textualmente, Hoare diz: o prop
osito de cada parte
consiste unicamente em sua contribuic
ao para o prop
osito do todo. Assim, em E + F,
ou
nico prop
osito de E e computar o n
umero que sera o operando esquerdo do operador
+. Isto signica que seu prop
osito nao inclui qualquer efeito colateral.
3. Independ
encia das partes. Este princpio apregoa que os signicados de duas partes,
n
ao sobrepostas, podem ser entendidos de forma completamente independente, ou seja, E
pode ser entendida independentemente de F e vice versa. Isto acontece porque o resultado
computado por um operador depende apenas dos valores de suas entradas.
4. Aplica
c
oes recursivas. Este princpio se refere ao fato de que as express
oes aritmeticas
sao construdas pela aplicacao recursiva de regras uniformes. Isto signica que se n
os
sabemos que E e F sao expressoes, entao sabemos que E + F tambem e uma expressao.
5. Interfaces pequenas. As expressoes aritmeticas tem interfaces pequenas, porque cada
operacao aritmetica tem apenas uma sada e apenas uma ou duas entradas. Alem disso,
3
cada uma das entradas e sadas e um valor simples. Assim, a interface entre as partes e
clara, pequena e bem controlada.
6. Estruturas manifestas. Este princpio se refere ao fato de que os relacionamentos estruturais entre as partes de uma express
ao aritmetica seja obvio. Uma expressao e uma
sub-expressao de uma expressao se estiver textualmente envolvida nela. Tambem duas expressoes nao estao estruturalmente relacionadas se elas nao se sobrepuzerem de qualquer
forma.
Estas caractersticas sao verdadeiras em relacao a` programacao estruturada, no entanto, nem
sempre elas sao assim entendidas. A este respeito, John Hughes fez algumas reexoes, analisando a import
ancia da programacao estruturada [15], fazendo um paralelo com as linguagens
funcionais. Em sua an
alise, ele cita que quando se pergunta a alguem o que e programacao
estruturada, normalmente se tem, uma ou mais, das seguintes respostas:
e uma programacao sem gotos,
e uma programacao onde os blocos n
ao tem entradas ou sadas m
ultiplas ou
e uma programacao onde os programas nela escritos s
ao mais trat
aveis matematicamente.
Apesar de todas as respostas acima serem corretas, no que se refere `a caracterizacao deste tipo
de programacao, elas nao sao conclusivas. Aludem ao que a programacao estruturada n
ao e ,
mas nao dizem o que realmente e a programacao estruturada. Na realidade, uma resposta
coerente para a pergunta sobre a programacao estruturada pode ser: programas estruturados
s
ao programas construdos de forma modular.
Esta e uma resposta armativa e que atinge o cerne da quest
ao. A construcao de programas
modulares e responsavel pela grande melhoria na construcao de software, sendo a tecnica respons
avel pelo not
orio aumento de produtividade de software que ultimamente tem se vericado.
Este aumento de produtividade se verica porque:
modulos pequenos podem ser codicados mais facilmente e mais rapidamente,
modulos de prop
osito geral podem ser reutilizados, ou seja, maior produtividade e
os modulos podem ser testados e compilados de forma independente, facilitando muito a
depuracao dos programas.
Mas por que a modularidade e tao determinante? A resposta e imediata: na modularidade,
os problemas sao decompostos em problemas menores e as solucoes para estes sub-problemas sao
mais faceis de serem encontradas. No entanto, estas pequenas solucoes devem ser combinadas
para representar uma solucao para o problema original como um todo.
Modula II, Ada, Pascal, C, C++, Standard ML, Haskell, Java, Eiel e todas as modernas linguagens de programacao, independente do paradigma utilizado, foram projetadas ou adaptadas
depois para serem modulares.
O que
e uma linguagem funcional?
Ainda segundo Hughes, um caso semelhante acontece quando se pergunta a alguem sobre o
que e programacao funcional. Normalmente, se tem como resultado uma ou mais das seguintes
respostas:
4
O processo de compila
c
ao de linguagens
Programar e modelar problemas do mundo real ou imagin
ario em um computador, usando algum
paradigma de programacao. Desta forma, os problemas sao modelados em um nvel de abstracao
bem mais alto atraves de especicacoes formais, feitas utilizando uma linguagem de especicacao
formal. Existem varias linguagens de especicacao formal: Lotos, Z, VDM, Redes de Petri, entre
outras. A escolha de uma delas esta diretamente ligada ao tipo da aplicacao e `a experiencia
do programador. Por exemplo, para especicar dispositivos de hardware e mais natural se
usar Redes de Petri, onde pode-se vericar a necessidade de sincronizacao e podem ser feitas
simulacoes para a an
alise de desempenho ou detectar a existencia, ou n
ao, de inconsistencias.
Muitas ferramentas gr
acas ja existem e sao utilizadas na analise de desempenho de dispositivos especicados formalmente e podem ser feitos prototipos r
apidos para se vericar a adequabilidade das especicacoes `as exigencias do usu
ario, podendo estes prot
otipos serem parte
integrante do contrato de trabalho entre o programador e o contratante. Mais importante que
isto, a especicacao formal representa um prova da corretude do programa e que ele faz exatamente o que foi projetado para fazer. Isto pode ser comparado com a garantia que um usu
ario
tem quando compra um eletrodomestico em uma loja. Esta garantia nao pode ser dada pelos
testes, uma vez que eles so podem vericar a presenca de erros, nunca a ausencia deles. Os
testes representam uma ferramenta importante na ausencia de uma prova da corretude de um
5
programa, mas n
ao representam uma prova. O uso de testes requer que eles sejam bem projetados e de forma objetiva, para que tenham a sua existencia justicada. Com a especicacao
pronta, ela deve ser implementada em uma linguagem de programacao.
Esta mesma metodologia foi tentada por alguns pesquisadores na traducao de programas codicados em linguagens funcionais para programas em linguagem de m
aquina, mas os resultados
n
ao foram animadores [22]. Os c
odigos executaveis gerados eram todos de baixo desempenho.
Por este motivo, os pesquisadores da area de implementacao de linguagens funcionais se viram
obrigados buscar outras alternativas de compilacao para estas linguagens.
A implementa
c
ao de linguagens funcionais
Como ja mencionado anteriormente, as linguagens funcionais apresentam caractersticas u
nicas,
como as funcoes de alta ordem, polimorsmo e lazy evaluation. Estas caractersticas sao proporcionadas por particularidades apresentadas pela programacao aplicativa ou funcional. Estas particularidades s
ao: a transparencia referencial, a propriedade de Church-Rosser, a independencia
na ordem de avaliacao e as interfaces manifestas, que serao objeto de estudo no Captulo 1
desta Apostila. Para que estas particularidades estejam presentes, e necessario que, durante a
execucao, muitas estruturas permanecam ativas na heap para que possam ser utilizadas mais
tarde. Estas caractersticas tem impedido a criacao de codigos executaveis enxutos e de bons
desempenhos.
Por este motivo, outras tecnicas de implementacao foram pesquisadas. Uma que tem apresentado resultados promissores consiste na traducao de programas codicados em linguagens
funcionais para programas em uma linguagem intermedi
aria, como na traducao das linguagens
4
aria, em vez da linguagem Asimperativas, mas utilizando -calculo como linguagem intermedi
sembly [21]. J
a existem metodos ecientes de traducao de programas codicados no -calculo
para programas em linguagem de m
aquina. Dessa forma, o problema agora se restringe a`
traducao dos programas escritos nas linguagens funcionais para programas em -calculo. Este
processo esta mostrado na Figura 3.
-c
alculo e uma teoria de funco
es que ser
a vista no Captulo 2 desta Apostila.
M
aquinas abstratas
Uma tecnica usada com sucesso na implementacao de linguagens funcionais consiste na implementacao do -calculo usando a reducao das -expressoes para -expressoes mais simples, ate
atingir uma forma normal, se ela existir. O resultado desta tecnica foi um sistema que cou
conhecido na literatura como m
aquinas abstratas. A primeira m
aquina abstrata foi desenvolvida
por Peter Landin em 1964 [20], que ganhou o alcunha de m
aquina SECD, devido ao seu nome
(Stack, Environment, Code, Dump). A maquina SECD usa a pilha S para a avaliacao das
-expressoes Codicadas, utilizando o ambiente E.
Uma otimizacao importante na m
aquina SECD foi transferir alguma parcela do tempo de
execucao para o tempo de compilacao. No caso, isto foi feito transformando as expressoes com
notacao inxa para a notacao polonesa reversa que e adequada para ser executada em pilha,
melhorando o ambiente de execucao. Para diferenciar da m
aquina SECD, esta maquina foi
chamada de SECD2.
Em 1979, David Turner [36] desenvolveu um processo de avaliacao de expressoes em SASL
(uma linguagem funcional) usando o que cou conhecida como a m
aquina de combinadores.
5
oes, em vez de
Ele utilizou os combinadores S, K e I do -calculo para representar express
-expressoes, utilizando para isto um dos combinadores acima citados, sem vari
aveis ligadas [7].
Turner traduziu diretamente as express
oes em SASL para combinadores, sem passar pelo estagio
intermedi
ario das -expressoes. A maquina de reducao de Turner foi chamada de M
aquina
de Redu
c
ao SK e utilizava a reducao em grafos como metodo para sua implementacao. Esta
maquina teve um impacto muito intenso na implementacao de linguagens aplicativas, pelo ganho
em eciencia, uma vez que ela n
ao utilizava o ambiente da maquina SECD, mas tirava partido
do compartilhamento que conseguia nos grafos de reducao.
Um outro pesquisador que se tornou famoso no desenvolvimento de m
aquinas abstratas foi
Johnsson, a partir de 1984, quando ele deu incio a uma serie de publicacoes [16, 17, 18] sobre este
tema, culminando com sua Tese de Doutorado, em 1987 [19], onde ele descreve uma maquina
abstrata baseada em supercombinadores que eram combinadores abstrados do programa de
usu
ario para algumas necessidades particulares. A maquina inventada por Johnsson cou conhecida pela M
aquina G e se caracterizou por promover uma melhoria na granularidade dos
programas, que era muito na na m
aquina de reducao de Turner. Uma otimizacao importante
desta maquina foi a utilizacao de uma segunda pilha na avaliacao de expressoes aritmeticas ou
outras expressoes estritas.
Combinadores e supercombinadores s
ao temas do -c
alculo, objeto de estudo do Captulo 2, deste trabalho.
de Pernambuco [8].
Alem destas, uma que tem se destacado com excelentes resultados e a maquina CMC,
tambem de Rafael [21, 8], onde um programa codicado em SASL e traduzido para um programa
em Ansi C. Este processo esta mostrado na Figura 4.
A escolha da linguagem C se deve ao fato de que os compiladores de C geram codigos reconhecidamente port
ateis e ecientes. A maquina CMC e baseada nos combinadores categoricos
que se fundamentam na Teoria das Categorias Cartesianas Fechadas, recentemente utilizada em
diversas areas da Computacao, sendo hoje um tema padr
ao do discurso, nos grandes encontros
e eventos na area da Inform
atica [9].
Esta Apostila
Esta Apostila e composta desta Introducao e 6 (seis) Captulos. Nesta Introducao, e colocada,
de forma resumida, a import
ancia das linguagens funcionais e a necessidade de estudar o calculo e justicar sua escolha como a linguagem intermedi
aria entre as linguagens funcionais
necessario saber que as linguagens funcionais s
e as linguagens de m
aquina. E
ao importantes
porque aumentam a modularidade dos sistemas atraves das funcoes de alto nvel e do mecanismo
de avaliacao preguicosa.
O Captulo 1 e dedicado a` fundamentacao das linguagens funcionais, abordando as principais diferencas entre elas e as linguagens de outros paradigmas. O mundo das linguagens de
programacao e dividido entre o mundo das express
oes e o mundo das atribuicoes, evidenciando
as vantagens do primeiro mundo em relacao ao segundo.
No Captulo 2, e introduzido o -calculo, sua evolucao hist
orica e como ele e usado nos dias
atuais. A teoria e colocada de maneira simples e introdutoria, dado o objetivo da Apostila.
No Captulo 3, inicia-se a programacao em Haskell. Sao mostrados seus construtores e uma
serie de exemplos, analisando como as funcoes podem ser construdas em Haskell. S
ao mostrados
os tipos de dados primitivos adotados em Haskell e os tipos estruturados mais simples que sao as
tuplas. No Captulo, tambem sao mostrados os esquemas de provas de programas, juntamente
com varios exerccios, resolvidos ou propostos.
O Captulo 4 e dedicado a` listas em Haskell. Este Captulo se torna necessario, dada a
import
ancia que este tipo de dado tem nas linguagens funcionais. Neste Captulo, s
ao mostradas
as compreensoes ou expressoes ZF e tambem e mostrada a composicao de funcoes como uma
caracterstica apenas das linguagens funcionais, usada na construcao de funcoes. Um tema
importante e que e discutido neste Captulo se refere `as formas de provas da corretude de
programas em Haskell, usando inducao estrutural sobre listas. No Captulo s
ao mostrados
v
arios exemplos resolvidos e, ao nal, s
ao colocados v
arios exerccios `a apreciacao do leitor.
No Captulo 5, s
ao mostradas as type class, como formas de incluir um determinado tipo
de dados em em uma classe de tipos que tenham funcoes em comum, dando origem `a sobrecarga
como forma de polimorsmo. Tambem sao mostrados os tipos de dados algebricos, o sistema de
modulos adotado em Haskell, os tipos de dados abstratos e o tratamento de excecoes. O Captulo
termina com uma revisao sobre o sistema de avaliacao lazy, notadamente na construcao de listas
potencialmente innitas.
O Captulo 6 e dedicado a`s operacoes de entrada e sada em Haskell, evidenciado o uso de
arquivos ou dispositivos como sada ou como entrada. Este processo em Haskell e feito atraves
do mecanismo de acoes, cuja semantica representa o conte
udo principal do Captulo.
A Apostila termina com as principais referencias bibliogracas consultadas durante s sua
elaboracao.
10
Captulo 1
Programa
c
ao Funcional
We can now see that in a lazy implementation
based on suspensions, we can treat every function in the same way.
Indeed, all functions are treated as potentially non-strict
and their argument is automatically suspended.
Later, is and when it is needed, it will be
unsuspended (strictly evaluated).
(Antony D. T. Davie in [7])
1.1
Introdu
c
ao
A programacao funcional teve incio antes da invencao dos computadores eletronicos. No incio
do seculo XX, muitos matematicos estavam preocupados com a fundamentacao matematica, em
particular, queriam saber mais sobre os conjuntos innitos. Muito desta preocupacao aconteceu
por causa do surgimento, no nal do seculo XIX, de uma teoria que armava a existencia de varias
ordens de innitos, desenvolvida por George Cantor (1845-1918) [24]. Muitos matematicos, como
Leopold Kronecker (1823-1891), questionaram a existencia destes objetos e condenaram a teoria
de Cantor como pura enrolacao. Estes matematicos defendiam que um objeto matematico
so poderia existir se, pelo menos em princpio, pudesse ser construdo. Por este motivo, eles
caram conhecidos como construtivistas.
Mas o que signica dizer que um n
umero, ou outro objeto matem
atico, seja construtvel?
Esta ideia foi desenvolvida lentamente, ao longo de muitos anos. Guiseppe Peano (1858-1932),
um matematico, l
ogico e linguista, escreveu Formulaire de Mathematique (1894-1908), onde
mostrou como os n
umeros naturais poderiam ser construdos atraves de nitas aplicacoes da
funcao sucessor. Comecando em 1923, Thoralf Skolen (1887-1963) mostrou que quase tudo
da teoria dos n
umeros naturais poderia ser desenvolvido construtivamente pelo uso intensivo
de denicoes recursivas, como as de Peano. Para evitar apelos questionaveis sobre o innito,
pareceu razoavel chamar um objeto de construtvel se ele pudesse ser construdo em um n
umero
nito de passos, cada um deles requerendo apenas uma quantidade nita de esforco. Assim, nas
primeiras decadas do seculo XX, j
a existia consideravel experiencia sobre as denicoes recursivas
de funcoes sobre os n
umeros naturais.
A cardinalidade (quantidade de elementos) dos conjuntos nitos era f
acil de ser conhecida,
uma vez que era necessario apenas contar seus elementos. Ja para os conjuntos innitos, esta
tecnica nao podia ser aplicada. Inicialmente, era necess
ario denir o que era realmente um
conjunto innito. Foi denido que um conjunto era innito se fosse possvel construir uma
correspondencia biunvoca entre ele e um subconjunto pr
oprio dele mesmo. Foram denidos os
conjuntos innitos enumer
aveis, que foram caracterizados pelos conjuntos innitos para os quais
11
1.2
Hist
orico
12
60, para o -calculo. Baseados neste estudo, Strachey e Scott construiram um metodo de denicao da semantica de linguagens de programacao, conhecido como sem
antica denotacional.
Em essencia, a semantica denotacional dene o signicado de um programa, em termos de um
programa funcional equivalente.
No entanto, a programacao aplicativa, que havia sido investigada por um reduzido n
umero
de pesquisadores nos anos de 1960 e 1970, passou a receber uma atencao bem maior apos 1978,
quando John Backus, o principal criador do FORTRAN, publicou um paper onde fez severas
crticas `as linguagens de programacao convencionais, sugerindo a criacao de um novo paradigma
de programacao. Ele prop
os o paradigma chamado de programac
ao funcional que, em essencia,
e a programacao aplicativa com enfase no uso de funcionais, que s
ao funcoes que operam sobre
outras funcoes. Muitos dos funcionais de Backus foram inspirados pela linguagem APL, uma
linguagem imperativa, projetada na decada de 1960, que provia operadores poderosos, sem
atribuicao, sobre estruturas de dados. A partir desta publicacao, o n
umero de pesquisadores na
area de linguagens de programacao funcional tem aumentado signicativamente.
1.3
Programa
c
ao com express
oes
Para MacLennan [24] qualquer linguagem de programacao se divide em dois mundos, a saber:
o mundo das express
oes (aritmeticas, relacionais, booleanas, etc.) e o mundo das atribuicoes.
No primeiro caso, o u
nico objetivo e encontrar o valor de uma expressao atraves de um processo
de avaliac
ao. J
a no segundo caso, o mundo das atribuicoes e dividido em dois tipos. O
primeiro altera o controle do uxo de um programa, usando comandos de selecao, como if, for,
while, repeat, goto e chamadas a procedimentos. O segundo tipo de atribuicao altera o estado
da memoria (principal ou secund
aria) do computador. Nos dois tipos, a palavra chave e alterar
alguma coisa; no primeiro tipo, altera o uxo de controle e, no segundo, altera o estado da
maquina.
No mundo das atribuicoes, a ordem em que as coisas sao feitas tem importancia fundamental.
Por exemplo, e seq
uencia
i = i + 1;
a = a i;
tem um efeito diferente da seq
uencia, a seguir, onde a ordem e invertida.
a = a i;
i = i + 1;
Analisemos agora a expressao z = (2 a y + b) (2 a y + c). A expressao do lado direito
do sinal de atribuicao contem uma sub-expressao em comum (2 a y) e qualquer compilador,
com um mnimo de otimizacao, transformaria este fragmento de c
odigo, na seguinte forma:
t = 2 a y;
z = (t + b) (t + c);
No mundo das express
oes, e seguro promover esta otimizacao porque qualquer sub-express
ao,
no caso 2 a y, sempre tem o mesmo valor. Analisemos agora, uma situacao similar no mundo
das atribuicoes.
Sejam as express
oes
y = 2 a y + b;
z = 2 a y + c;
onde tambem vericamos uma sub-expressao em comum (2 a y). Se for realizada a mesma
fatoracao anterior, teremos:
13
t = 2 a y;
y = t + b; z = t + c;
Esta otimizacao altera o valor da vari
avel z, porque os valores de y sao diferentes nas duas
ocorrencias da sub-express
ao 2*a*y. Portanto, n
ao e possvel realizar esta otimizacao. Apesar
da an
alise sobre a existencia, ou n
ao, de dependencia entre sub-expressoes poder ser feita por um
compilador, isto requer tecnicas sosticadas de analise de dependencias no uxo. Tais an
alises,
normalmente s
ao caras para serem realizadas e difceis de serem implementadas corretamente.
De forma resumida, e f
acil e seguro realizar estas otimizacoes nas expressoes e difcil de serem
feitas no mundo das atribuicoes. Fica clara a vantagem das expressoes sobre as atribuicoes. O
prop
osito da programacao funcional e extender as vantagens das expressoes para as linguagens
de programacao.
1.4
Independ
encia da ordem de avalia
c
ao
Figura 1.1: Arvore
representativa da express
ao (3ax+b)(3ax+c).
Uma express
ao e dita pura quando n
ao realiza qualquer operaca
o de atribuica
o, explcita ou implcita.
15
1.5
Transpar
encia referencial
Vamos novamente considerar o contexto de avaliacao da secao anterior. Podemos vericar que
se uma pessoa fosse avaliar manualmente a expressao (3ax + b)(3ax + c) jamais iria avaliar a
sub-expressao 3ax, que neste contexto e 18, duas vezes. Uma vez avaliada esta sub-expressao, o
16
1.6
Interfaces manifestas
A evolucao da notacao matematica ao longo dos anos tem permitido que ela seja utilizada
com sucesso, para exibir muitas propriedades. Uma destas propriedades se refere a`s interfaces
manifestas, ou seja, a`s conexoes de entradas e sadas entre uma sub-expressao e a expressao que
a envolve. Consideremos a express
ao 3 + 8. O resultado desta adicao depende apenas das
entradas para a operacao (3 e 8) e elas estao mostradas de forma clara na expressao, ou seja,
sao colocadas a` esquerda e `a direita do operador +. N
ao existem entradas escondidas para
este operador.
Vamos agora considerar o caso de uma funcao em uma linguagem de programacao convencional, onde o resultado a ser retornado pela funcao depende de uma ou mais vari
aveis locais
ou n
ao locais. Assim, chamadas sucessivas com as mesmas entradas podem produzir resultados
distintos. Por exemplo, seja a funcao f denida da seguinte forma:
Nao existe uma forma de se conhecer o valor de f(3) sem antes saber o valor da vari
avel n
ao
local, a. O valor de a e atualizado em cada chamada a f, ent
ao f(3) tem valores diferentes em
cada chamada. Esta situacao caracteriza a existencia de interfaces escondidas para a funcao f,
tornando difcil, ou mesmo impossvel, se prever o comportamento da funcao.
Consideremos, novamente, a expressao (2ax + b) (2ax + c). Como pode ser observado, as
entradas para o primeiro operador + (2ax e b) estao manifestas. O papel da sub-expressao
2ax + b, dentro da express
ao completa, tambem e manifesto, ou seja, ela representa o argumento
esquerdo do operador de multiplicacao. N
ao existem sadas escondidas ou side eects na adicao.
Portanto, tanto as entradas como as sadas desse operador sao determinadas muito facilmente.
As entradas sao as sub-expressoes 2ax e b em qualquer lado do operador e a sada e liberada
para a express
ao envolvente.
Pode-se sumarizar as caractersticas das interfaces manifestas, da seguinte forma: as expressoes podem ser representadas por arvores e a mesma arvore representa tanto a estrutura
sint
atica de uma expressao quanto a forma como os dados uem na express
ao. As sub-expressoes
que se comunicam entre si podem sempre ser colocadas em posicoes contguas na arvore, como
na forma escrita da express
ao. Esta caracterstica nao e valida no mundo das atribuicoes, uma
vez que as variaveis alteraveis permitem comunicacao n
ao local. Em geral, o gr
aco do uxo
de dados n
ao e uma arvore e pode ser uma estrutura muito diferente da arvore sint
atica. Assim, pode n
ao ser possvel colocar juntas as partes comunicantes, de forma que suas interfaces
sejam obvias. A identidade estrutural das dependencias de dados e das dependencias sintaticas
e certamente uma das principais vantagens das express
oes puras.
18
1.7
Fun
c
oes e express
oes aplicativas
Vamos agora analisar as avaliacoes das expressoes aritmeticas. Estas expressoes sao estruturalmente simples, ou seja, s
ao construdas de forma uniforme pela aplicacao de operacoes
aritmeticas a seus argumentos. Mais importante que isto, estas operacoes sao funcoes puras, ou
seja, sao mapeamentos matematicos de entradas para sadas. Isto signica que o resultado de
uma operacao depende apenas de suas entradas. Alem disso, uma expressao construda a partir
de funcoes puras e constantes tem sempre o mesmo valor. Por exemplo, seja a funcao pura f
denida da seguinte forma:
f (u) = (u + b)(u + c)
em um contexto em que b = 3 e c = 2. A funcao f e pura porque e denida em termos de funcoes
puras que s
ao as operacoes aritmeticas de adicao e multiplicacao. Vamos considerar agora, a
avaliacao de f (3ax) em um contexto em que a = 2 e x = 3. Como a avaliacao de expressoes
aritmeticas independe da ordem de avaliacao, assim tambem sera a avaliacao de f . Isto signica
que o argumento (3ax) de f pode ser avaliado antes ou depois da substituicao; o resultado ser
a
sempre o mesmo. Se ele for avaliado antes, a seq
uencia de reducoes sera:
f (3ax) f (3 2 3) f (18) (18 + b) (18 + c) (18 + 3) (18 2) 21 16 336
Se, no entanto, a avaliacao de 3ax for deixada para ser feita ap
os a substituicao, a seq
uencia
de reducoes sera
f (3ax) (3ax + b) (3ax + c) (3 2 3 + b) (3ax + c) . . . 336
Estas duas seq
uencias de avaliacao correspondem aos metodos de passagem de parametro
por valor e por nome, respectivamente, nas linguagens de programacao comuns. A passagem
de par
ametros por valor, normalmente, e mais eciente porque o argumento e avaliado apenas
uma vez, no entanto o que e mais importante e o fato de que o valor da expressao e o mesmo,
independente da ordem de avaliacao adotada. No entanto, apesar da ordem de avaliacao escolhida n
ao ter inuencia no valor nal da express
ao, ela pode inuenciar sobre o termino, ou
n
ao, do processo de avaliacao. Por exemplo, a avaliacao da funcao f (x) 1 para x = 1/a em
um contexto em que a seja igual a zero tem diferenca nas duas ordens de avaliacao. Se 1/a for
avaliado antes da substituicao a avaliacao sera indenida e n
ao termina. Se for deixada para ser
feita depois da substituicao, o resultado ser
a 1. No primeiro caso, a avaliacao e dita estrita e
no segundo caso, ela e dita n
ao estrita em relacao a seu argumento x.
A programacao aplicativa, ou funcional, e freq
uentemente disting
uida da programacao imperativa que adota um estilo que faz uso de imperativos ou ordens. O mundo das atribuicoes e
caracterizado tambem por ordens, por exemplo, troque isto!, v
a para tal lugar!, substitua
isto! e assim por diante. Ao contrario, o mundo das express
oes envolve a descricao de valores,
por isto, o termo programac
ao orientada por valores.
Na programacao aplicativa, os programas tomam a forma de expressoes aplicativas. Uma
expressao deste tipo e uma constante (2, , e, etc) ou e composta totalmente de aplicacao de
funcoes puras a seus argumentos, que tambem sao expressoes aplicativas. Em BNF, as expressoes
aplicativas sao denidas da seguinte forma:
<EA> ::= <id> (<EA>, ...)
| <literal>
| <id>
A estrutura aplicativa se torna mais clara se as expressoes forem escritas na forma pre-xa,
ou seja, sum(prod (prod (2, a), x), b) em vez da forma usual 2ax+b.
19
1.8
Defini
c
ao de fun
c
oes
A programacao funcional usando a linguagem Haskell e o principal objetivo desta Apostila. Isto
signica que devemos estudar formas de passagens das funcoes matematicas para funcoes codicadas em Haskell. Do ponto de vista matematico, as funcoes sao denidas sobre conjuntos,
domnio e imagem, mas nao se tem qualquer preocupacao sobre as formas como elas sao executadas para encontrar um valor resultado. J
a do ponto de vista da computacao, esta preocupacao
existe e e muito importante. No mundo da computacao, as funcoes sao declaradas sobre tipos
e leva-se em conta o algoritmo utilizado para implementa-las. A diferenca de desempenho de
um algoritmo em relacao a um outro, denido para executar a mesma funcao, tem importancia
fundamental no mundo da computacao. Apesar desta diferenca de pondo de vista entre estes
dois mundos, o processo de passagem de um para o outro e quase um processo de traducao
direta. Assim, neste Captulo, as funcoes serao denidas como na Matem
atica e deixamos a
an
alise do ponto de vista computacional para ser feita no Captulo 3.
Defini
c
oes de func
oes por enumerac
ao
Uma funcao e uma associacao de valores pertencentes a um conjunto de partida, o domnio, com
valores pertencentes a um conjunto de chegada, o contra-domnio ou imagem da funcao. Com
20
esta denicao em mente, uma funcao pode ser representada de duas maneiras. A primeira delas e
exibir todos os pares do tipo (entrada, sada), sendo esta uma denicao extensionista, tambem
conhecida como denicao por enumerac
ao, uma vez que todos os seus elementos sao exibidos. A
segunda maneira de representar uma funcao e exibir uma propriedade que apenas os elementos
desta funcao a tem. Isto signica a exibicao de uma regra que informa como cada elemento do
domnio e processado, transformando-o em um valor que e associado a um u
nico elemento da
imagem. Esta forma de apresentacao de uma funcao e conhecida como intencionista.
Por exemplo, a funcao booleana not tem como domnio e contra-domnio o conjunto {True,
False} e pode ser denida, por extens
ao ou enumeracao, da seguinte forma:
not True = False
not False = True
De forma similar, as funcoes or (disjuncao) e and (conjuncao) tambem podem ser denidas
por enumeracao, da seguinte maneira:
or
or
or
or
(False, False)
(False, True)
(True, False)
(True, True)
=
=
=
=
False
True
True
True
and
and
and
and
(False, False)
(False, True)
(True, False)
(True, True)
=
=
=
=
False
False
False
True
Defini
c
ao de funco
es por composi
c
ao
Nao e difcil entender que as denicoes de funcoes por enumerac
ao so tem sentido se o domnio
e o contra-domnio forem nitos e de cardinalidade pequena. A grande maioria das funcoes nao
pode ser denida desta forma. Uma alternativa e denir as funcoes pela composic
ao de outras
funcoes ja denidas. Como exemplo, a funcao implica pode ser denida da seguinte forma:
implica (x, y) = or (not x, y)
Neste caso, e necessario saber como as funcoes a serem compostas sao aplicadas. A aplicacao
de uma funcao se torna simplesmente um processo de substituicao das funcoes primitivas. Por
exemplo, para avaliar a funcao implica (False, True) e necessario apenas fazer a substituicao
dos argumentos pelas aplicacoes das funcoes primitivas.
implica (False, True) = or (not False, True) = or (True, True) = True
Este processo e independente do domnio, ou seja, e independente de quando se est
a tratando
com funcoes sobre n
umeros, ou funcoes sobre caracteres, ou funcoes sobre arvores, ou qualquer
outro tipo. Em outras palavras, se a funcao f for denida por
f (x) = h(x, g(x))
entao sabemos que
f (u(a)) = h(u(a), g(u(a)))
independente das denicoes de g, h, u ou da constante a.
Frequentemente, a denicao de uma funcao n
ao pode ser expressa pela composicao simples
de outras funcoes, sendo necessaria a denicao da funcao para v
arios casos. Por exemplo, a
funcao que retorna o sinal algebrico de uma vari
avel pode ser denida da seguinte forma:
21
1,
se x > 0
0,
se
x=0
sinalg(x) =
1, se x < 0
De forma similar, a diferenca absoluta entre x e y pode ser denida da seguinte forma:
dif abs(x, y) =
x y, se x > y
y x, se x y
Defini
c
ao de funco
es por recurs
ao
Algumas situacoes existem em que e necessario denir uma funcao em termos de um n
umero
innito de composicoes. Por exemplo, a multiplicacao de n
umeros naturais () pode ser denida
por innitas aplicacoes da funcao de adicao (+). Sen
ao vejamos:
mn=
0,
n,
se
se
n + n,
se
n + n + n, se
..
.
m=0
m=1
m=2
m=3
mn=
0,
se m = 0
n + (m 1) n, se m > 0
Defini
c
ao explcita de vari
aveis
Vamos considerar agora as denic
oes explcitas de funcoes, analisando inicialmente, as denicoes
explcitas de variaveis. Uma denicao de uma vari
avel e explcita se ela aparece no lado esquerdo
da equacao e n
ao aparece no lado direito. Por exemplo, a equacao
y = 2ax
dene explicitamente a variavel y. As denicoes explcitas tem a vantagem de que elas podem
ser interpretadas como regras de reescritas que nos informam como substituir uma classe de
expressoes por outra. Por exemplo, a denicao anterior de y, implica na regra de reescrita
y 2ax que nos diz como eliminar a vari
avel y em qualquer f
ormula, onde y ocorra. Por
2
exemplo, para eliminar y da expressao 3y + 5y + 1 aplica-se a regra de reescrita acima, para se
obter
3y 2 + 5y + 1 3(2ax)2 + 5(2ax) + 1
A nocao de denicao explcita de vari
aveis pode ser extendida aos conjuntos de equacoes
simultaneas. Um conjunto de vari
aveis e denido explicitamente por um conjunto de equacoes,
se
as operacoes forem individualmente explcitas e
elas puderem ser ordenadas, de forma que nenhuma delas use em seu lado direito uma
vari
avel j
a denida anteriormente na lista.
Por exemplo, o conjunto de equacoes
y =2ax
x=2
a=3
y 2ax
x2
a3
Estas regras podem ser aplicadas na seguinte ordem: a primeira delas pode ser aplicada ate
que n
ao exista mais y, em seguida a segunda e aplicada ate que n
ao exista mais x e, nalmente,
a terceira e aplicada ate que n
ao exista mais a. Desta forma teremos a seguinte seq
uencia de
reducoes:
y 2ax2a2 232
Defini
c
ao implcita de vari
aveis
Dizemos que uma vari
avel e denida implicitamente se ela for denida por uma equacao onde
ela aparece nos dois lados da equacao. Por exemplo,
2a = a + 3
dene implicitamente a como 3. Para encontrar o valor de a e necessario resolver a equacao
usando regras da algebra. O processo de solucao pode ser visto como uma forma de converter
uma denicao implcita em uma denicao explcita, mais usual, uma vez que uma denicao
implcita n
ao pode ser convertida diretamente em uma regra de reescrita. Assim, a equacao
23
2a = a + 3
n
ao nos informa explicitamente o que deve ser substitudo por a na expressao 2ax, por exemplo.
Alem disso, as regras de reescrita que resultam de denicoes explcitas sempre terminam, ou seja,
a aplicacao repetida das regras de reescritas elimina todas as ocorrencias da variavel denida.
Por outro lado, e possvel escrever denicoes implcitas que n
ao terminam, ou seja, nada
denem. Considere, como exemplo, a denicao implcita
a=a+1
Apesar de sabermos, claramente, que esta equacao n
ao tem solucao, este fato pode n
ao ser
t
ao obvio, em casos mais complexos. Se, ingenuamente, interpretarmos esta equacao como a
regra de reescrita
a a+1
ent
ao chegaremos a um nao determinismo na seguinte seq
uencia de reducoes:
2a 2(a + 1) 2((a + 1) + 1) . . .
As vari
aveis tambem podem ser denidas implicitamente por conjuntos de equacoes simult
aneas. Por exemplo, as equacoes
2a = a + 3
d 1 = 3d + a
denem implicitamente a = 3 e d = 2. Podemos tambem ter denicoes implcitas em que as
vari
aveis nao aparecem nos dois lados da mesma equacao. Por exemplo, as equacoes
2a = x
x+1=a+4
em que, nem a nem x aparece nos dois lados de uma mesma equacao, denem implicitamente
a e x. Neste caso, nao existe qualquer forma de ordenacao destas equacoes de maneira que
as u
ltimas equacoes nao facam uso de vari
aveis ja denidas nas equacoes anteriores. A forma
implcita pode ser observada pela transformacao das duas equacoes em uma so, ou seja,
2a + 1 = a + 4
Em resumo, uma denicao explcita nos informa o que uma determinada coisa e, enquanto
uma denicao implcita estabelece algumas propriedades que esta coisa deve apresentar, exigindo que apenas esta coisa tenha estas propriedades. A determinacao do que esta coisa e
exige um processo de solucao.
Defini
c
oes explcitas e implcitas de func
oes
Ap
os termos analisado as denicoes explcitas e implcitas das variaveis, vamos agora considerar
como estas denicoes sao aplicadas ao caso das funcoes. Por exemplo, as duas equacoes, a seguir,
denem implicitamente a funcao implica.
and [p, implica (p, q)] = and (p, q)
and [not p, implica (p, q)] = or [not p, and (not p, q)]
Estas equacoes nao podem ser usadas explicitamente para avaliar uma expressao como implica (True, False). Usando a algebra booleana, estas equacoes podem ser resolvidas para se
chegar `a seguinte denicao explcita:
implica (p, q) = or (not p, q)
24
A denicao explcita permite que implica (True, False) seja avaliada usando substituicao.
Uma vantagem da programacao funcional e que, como a algebra elementar, ela simplica a
transformacao de uma denicao implcita em uma denicao explcita. Isto tem uma importancia
muito forte porque as especicacoes formais de sistemas de softwares, frequentemente, tem a
forma de denicoes implcitas, enquanto as denicoes explcitas s
ao, normalmente, f
aceis de
serem transformadas em programas. Assim, a programacao funcional prove uma forma de se
passar das especicacoes formais para programas satisfazendo estas especicacoes.
Exerccios.
1. Mostre que a denicao explcita da funcao implica (anterior) satisfaz a sua denicao
implcita.
2. Mostre que a denicao explcita da funcao implica e a u
nica solucao para a sua denicao
implcita, ou seja, nenhuma outra funcao booleana satisfaz estas duas equacoes, apesar de
que devem existir outras formas de expressar esta mesma funcao. Sugest
ao: usar Tabelaverdade.
Deve ser notado que as denicoes recursivas sao implcitas, por natureza. No entanto, como
seu lado esquerdo e simples, ou seja, composto apenas pelo nome da funcao e seus argumentos,
elas podem ser convertidas facilmente em regras de reescritas. Por exemplo, as duas equacoes
seguintes constituem uma denicao recursiva de fatorial, para n 0.
fat n = n fat (n-1),
fat 0 = 1
se n > 0
se n > 0
Estas regras de reescritas nos dizem como transformar uma formula contendo fat. A realizacao destas transformacoes, no entanto, n
ao elimina, necessariamente, a funcao da f
ormula.
Por exemplo,
2 + f at 3 2 + 3 f at (3 1)
No entanto, se a computacao termina, ent
ao a aplicacao repetida das regras de reescrita
eliminar
a fat da f
ormula
2 + 3 f at 2 2 + 3 2 f at 1
2 + 3 2 1 f at 0
2+3211
1.9
Resumo
Este Captulo se relaciona com a fundamentacao teorica das linguagens funcionais. Esta fundamentacao e fortemente conectada com a Matematica e isto tem como vantagem primeiramente a
constatacao de essas linguagens tem um embasamenteo teorico sob o qual elas se fundamentam
e depois, sendo este embasamento oriundo da Matematica, torna estas linguagens mais trat
aveis
matematicamente e mais faceis de terem seus programas provados.
O objetivo deste Captulo foi mostrar a programacao funcional como uma programacao sem
atribuicoes, da mesma maneira que a programacao estruturada e uma programacao sem gotos.
Uma maquina, ao executar um programa estruturado, est
a, de fato, executando gotos, enquanto
25
26
Captulo 2
-c
alculo
Type theories in general date back
to the philosopher Bertrand Russell and beyond.
They were used in the early 1900s for the very specic
purpose of getting round the paradoxes that had shaken
the foundations of mathematics at that time,
but their use was later widened until they came to be part of
the logicians standard bag of techinica tools, especially in proof-theory.
(J. Roger Hindley in [13])
2.1
Introdu
c
ao
O -calculo foi desenvolvido por Alonzo Church no incio dos anos 30, como parte de um sistema
de l
ogica de ordem superior com o prop
osito de prover uma fundamentacao para a matematica,
dentro da losoa da escola logicista de Peano-Russel [22]. O -calculo e uma teoria que ressurge do conceito de funcao como uma regra que associa um argumento a um valor calculado
atraves de uma transformacao imposta pela denicao da funcao. Nesta concepcao, uma funcao
e representada por um grafo, onde cada n
o e um par (argumento, valor).
A l
ogica combinatorial foi inventada antes do -calculo, em cerca de 1920, por Moses
Sch
onnkel, com um prop
osito semelhante ao do -calculo [5]. No entanto, ela foi redescoberta 10 anos depois, em 1930, por Haskell B. Curry, que foi o maior respons
avel pelo seu
desenvolvimento ate cerca de 1970. Em 1935, Kleene e Rosser provaram que o -calculo e a
l
ogica combinatorial eram inconsistentes, o que provocou um desinteresse academico pelo tema.
No entanto, Curry n
ao desistiu de seu estudo e construiu uma extensao na l
ogica combinatorial
para ns de seus estudos, conseguindo sistemas mais fracos que a logica classica de segunda
ordem. Em 1975, Dana Scott e Peter Aczel mostraram que seu u
ltimo sistema apresentava
modelos interessantes [5]. Em 1941, Church desistiu de sua pesquisa inicial e apresentou uma
sub-teoria que lidava somente com a parte funcional. Essa sub-teoria, chamada de -calculo
foi mostrada ser consistente pelo teorema de Church-Rosser [22]. Usando o -calculo, Church
prop
os uma formalizacao da nocao de computabilidade efetiva pelo conceito de denibilidade
no -calculo. Kleene mostrou que ser denvel no -calculo e equivalente a` recursividade de
G
odel-Herbrand. Concomitantemente, Church formulou a sua conjectura associando a recursividade como a formalizacao adequada do conceito de computabilidade efetiva. Em 1936, Allan
Turing modelou a computacao automatica e mostrou que a nocao resultante (computabilidade
de Turing) e equivalente a` denibilidade no -calculo. Desta forma, o -calculo pode ser visto
como uma linguagem de programacao. Isto implica que podem-se analisar diversos tipos de
problemas de programacao, em particular, os relacionados com chamadas a procedimentos [22].
27
Curry e seus colegas [6], Barendregt [3] e outros desenvolveram extensivamente os aspectos
sint
aticos do -calculo, enquanto Scott [32], principalmente reportado por Stoy [34] e Schmidt
[31], se dedicou `a semantica da notacao, o que veio a facilitar a vida dos usu
arios futuros do
-calculo.
2.2
-express
oes
constantes embutidas
nomes de variaveis
combinacao ou aplicacao
abstracao ou funcao.
Apesar das denicoes acima serem claras, elas sao muito rgidas e, am alguns casos, podem
surgir d
uvidas sobre as regras de escopo, ou seja, ate que ponto em uma -expressao uma vari
avel
tem inuencia. Assim, as observacoes a seguir devem ser utilizadas para dirimir d
uvidas, por
acaso existentes.
Observa
c
oes importantes:
1. As vari
aveis sao representadas por letras romanas min
usculas.
2. As -expressoes completas sao representadas por letras romanas mai
usculas ou por letras
gregas min
usculas (exceto , , e que tem signicados especiais) ou por letras gregas
mai
usculas.
3. Apesar da rigidez mostrada na BNF das -expressoes, os parenteses devem ser usados apenas para resolver ambig
uidades [38]. Esta e uma convencao que diminui muito
o tamanho das -expressoes e deve ser utilizada, sempre que possvel. Por exemplo,
(x.(y.(. . . (z.(E)) . . .))) pode e deve ser escrita como xy . . . z.E, signicando que grupos de abstracoes ou funcoes sao associados pela direita. No entanto, grupos de combinacoes ou aplicacoes termos combinados sao associados pela esquerda, ou seja, E1 E2 E3 . . . En
signica (. . . ((E1 E2 )E3 ) . . . En ). Alem disso, a.CD representa (a.CD) e n
ao (a.C)(D),
estabelecendo que o escopo de uma vari
avel se extende ate o primeiro parentese descasado
encontrado a partir da vari
avel, ou atingir o nal da -expressao.
4. A escolha de constantes, de funcoes embutidas para serem utilizadas na manipulacao destas
constantes e/ou n
umeros e das funcoes para o processamento de listas e arbitr
aria.
28
Estas observacoes sao importantes porque elas podem ser utilizadas para dirimir algumas
d
uvidas que, com certeza, v
ao existir, principalmente por quem esta travando um primeiro
comum n
contacto com o processo de avaliacao no -calculo. E
ao se reconhecer imediatamente
o escopo de uma vari
avel e estas observacoes podem ser um meio importante para dirimir estas
d
uvidas.
Exemplos
S
ao exemplos de -expressoes:
1. y
2. 2
3. xy
4. (x . (xy))
5. ((y . y) (x . (xy)))
6. (x (x . y))x
7. (x . y . z . (xz)) (x . x)(x . x)
2.3
A sintaxe do -c
alculo
Nesta secao sera mostrada a sintaxe do -calculo, cando a semantica para uma pr
oxima.
Todas as aplicacoes de funcoes sao pre-xas, signicando que, por exemplo, a express
ao
(+( 2 3) ( 8 2)) tem seus parenteses externos redundantes podendo, e ate devendo,
serem retirados para evitar confusao visual.
Do ponto de vista de implementacao, um programa funcional deve ser visto como uma
expressao que vai ser avaliada.
Esta avaliacao se da pela selecao repetida de express
oes redutveis, conhecidas como redexes, e suas reducoes. Por exemplo, a -expressao (+( 2 3) ( 8 2)) apresenta dois redexes:
um ( 2 3) e o outro ( 8 2), apesar da expressao toda n
ao representar um redex porque
seus parametros ainda n
ao estao todos avaliados. A avaliacao da -expressao acima e feita
com a seguinte seq
uencia de reducoes:
(+( 2 3) ( 8 2)) (+ 6( 8 2)) (+ 6 16) 22
2.3.1
Aplica
c
ao de func
ao e currificac
ao
2.4
Fun
c
oes e constantes pr
e-definidas
Uma caracterstica importante do -calculo e a facilidade com que as funcoes podem ser construdas pelo usu
ario. No entanto, algumas delas juntamente com algumas constantes ja foram
pre-denidas e que podem ser utilizadas pelos usu
arios. Entre elas podem ser citadas:
funcoes matematicas: (+, -, *, /),
constantes (0, 1, 2, ...), NIL
funcoes logicas (AND, OR, NOT)
constantes (TRUE, FALSE)
caracteres constantes (a, b, ...)
funcao condicional IF: IF TRUE E1 E2 E1
IF FALSE E1 E2 E2
construtores de dados: (CONS, HEAD, TAIL) onde
HEAD (CONS a b) a
TAIL (CONS a b) b
Exemplos:
a) 5 4 1
b) AND TRUE FALSE FALSE
2.5
-abstrac
oes
};
Deve ser observado que as funcoes em uma linguagem de programacao convencional tem de
ter um nome, enquanto, no -calculo, as -abstracoes sao funcoes anonimas.
30
2.6
A sem
antica operacional do -c
alculo
A semantica operacional do -calculo diz respeito `as regras utilizadas para converter uma expressao em outra. Na realidade, existem 3 (tres) regras de conversao. Antes de serem mostradas estas regras, devemos denir alguns termos que sao utilizados na aplicacao destas regras.
Uma ideia central no estudo das linguagens de programacao, da notacao matematica e da
l
ogica simbolica e a de ocorrencia livre e ocorrencia ligada (ou conectada) de uma vari
avel. Esta
ideia, normalmente, provoca confusao em quem esta vendo o tema pela primeira vez. Assim, ele
sera introduzido de forma gradual e intuitiva, utilizando exemplos j
a conhecidos da Matematica.
Assim, vamos considerar o somat
orio:
n
i2 + 1
i=1
k2 + 1
k=1
x2 3xdx
t2 3tdt
n
(j 2 + i a)
j=1
n
(k2 + i a)
k=1
tem, exatamente, o mesmo signicado da anterior. Em uma expressao, uma ocorrencia nao
ligada de uma vari
avel e dita ser livre. As vari
aveis i, a e n ocorrem livre nesta expressao.
Se uma ocorrencia de uma vari
avel livre for trocada, o signicado da express
ao tambem sera
trocado. Alem disso, uma ocorrencia de uma vari
avel pode ser ligada em uma expressao e livre
em outra. Por exemplo, a vari
avel i e livre em
n
j2 + i a
j=1
e em
n
ai2 j 2
j=1
i!
n
j 2 + i a
j=1
O local da vinculacao (binding site) de um identicador determina seu escopo, que e a regiao
da expressao na qual o identicador ocorre ligado. Esta regi
ao, normamente, e indicada por
alguma convencao lexica, como os parenteses, colchetes ou chaves. Ainda nesta secao, sera visto
que esta regiao e o corpo da expressao.
Como visto, a troca de vari
aveis ligadas nao interfere no signicado da express
ao. No entanto,
esta troca nao pode ser feita para uma vari
avel que ocorra livre na express
ao, caso contrario a
expressao muda seu signicado. Por exemplo, a express
ao que representa a soma dos elementos
da linha i de uma matriz Amn e o somatorio
n
Aij
j=1
A vari
avel j ocorre ligada nesta expressao e, neste caso, pode ser trocada por outra, por
exemplo, k.
n
Aik
k=1
2.6.1
Formaliza
c
ao das ocorr
encias livres ou ligadas
Ap
os termos visto as nocoes de ocorrencias ligada e livre, como tambem a nocao de colisao de
identicadores de maneira totalmente informal, vamos agora formalizar estes conceitos.
Seja a -expressao (x . + x y) 4. Esta expressao informa que se trata de uma funcao sobre
a vari
avel x, onde o corpo desta funcao e (+ x y).
A vari
avel x pode ser pensada como um local onde o argumento 4 deve ser colocado. J
a
para a vari
avel y, este mesmo raciocnio n
ao pode ser aplicado, uma vez que nao existe este
local sinalisado pela variavel y seguida ap
os um . Isto implica que as duas vari
aveis (x e y)
tem status distintos.
Formalmente, dene-se: uma ocorrencia de uma vari
avel e ligada se existir uma -abstrac
ao
` qual esta vari
a
avel esteja ligada. Em caso contr
ario, a ocorrencia e livre. A Figura 2.1 mostra
um exemplo detalhado de vari
aveis livres e ligadas.
x . +(( y . + y z) 7) x
ocorrencias:
ligada
livre ligada
se x = y. Se x
= y, a ocorrencia de x e ligada.
se (x for livre em ) E (x
= y)
se (x for livre em ) OU (x for livre em )
Exemplos
1. x ocorre livre em x, em xy, em a . xy e em (a . xy) (x . xy).
2. x ocorre ligada em y, em x . xy, em (x . ax) (y), em x . abbx e em (a . xy)(x . xy).
3. N
ao existem variaveis livres nos combinadores I, K, S, , Y, (ver secao 2.7).
2.7
Combinadores
Identidade
Projecao
Composicao
Duplicacao
Usado na representacao de funcoes recursivas
Tambem usado na representacao de funcoes recursivas
33
Exerccios resolvidos
1. Identique nas express
oes abaixo aquelas que s
ao, ou n
ao, -expressoes:
a) a
Sim, a e uma vari
avel;
b) 9
Sim, 9 e uma constante;
c) ((b.b)(a.ab)
N
ao, os parenteses nao estao aninhados
corretamente;
d) (x.)a
Nao, x. n
ao e um termo;
e) ((x.y.y)(y.yyy))((i.i)(a.b)) Sim, porque contem apenas aplicacoes,
abstracoes e variaveis.
2. Identique nas express
oes abaixo as ocorrencias livres e as ligadas das variaveis
a) x.xx
As duas ocorrencias da variavel x sao conectadas
b) (x.y.x)x
Ou
ltimo x ocorre livre
c) (x.y.xx)xa As duas u
ltimas vari
aveis ocorrem livres
d) (x(x.y))x
Todas as variaveis ocorrem livres
Exerccios propostos
1. Justique porque as express
oes abaixo n
ao sao -expressoes:
(a) (x(y)z)
(b) (x.y.x((i.ii)(b.c.b)(b.c.b))
(c)
(d) x.x
2. Identique as ocorrencias livres e as ocorrencias ligadas das variaveis nas expressoes abaixo:
(a) (x.xwx)9
(b) (x.y.x)3b
(c) (x.yxx)(i.i)5
(d) (z.b.c.ac(bc)f )(b.c.b)(b.c.b)
(e) (x.y.y)w((z.zzz)(w.www))((a.a)(a.b))
3. Quais das seguintes expressoes sao combinadores?
34
(a) (x.xx)9
(b) (x.y.x)3
(c) (x.yxx)(i.i)5
(d) (a.b.c.ac(bc)f )(b.c.b)
(e) (x.y.y((z.zzz)(w.ww))(a.a)(a.x))
2.8
Regras de convers
oes entre -express
oes
2.8.1
-convers
ao
(x . x 1) (y. y 1)
2.8.2
-convers
ao
2.8.3
-convers
ao
1. (x . + xx) 5 + 5 5 = 10
2. (x . 3) 5 3
3. (x . (y . y x))4 5 (y . y 4)5 5 4 = 1.
Observa
c
oes:
1. Nas reducoes acima, deve ser observada a curricacao em acao, ou seja, as aplicacoes das
-abstracoes retornando uma outra funcao (-abstracao) como resultado.
2. Normalmente se abrevia (x . (y . E)) por x . y . E.
3. Para o caso de se ter uma funcao como argumento, as substituicoes sao realizadas da
mesma forma. Por exemplo,
(f . f 3)(x . + x 1) (x . + x 1)3 + 3 1 = 4
2.8.4
Nomea
c
ao
Deve ser tomado algum cuidado na escolha dos nomes dos parametros, uma vez que mais de
uma vari
avel pode ter o mesmo nome. Por exemplo,
(x . (x . + ( x 1))x 3)9 se -reduz para (x . + ( x 1))9 3 que se -reduz para +( 9 1)3
que e igual a + 8 3 que, nalmente, e igual a 11.
Deve ser observado que o x interno de ( x 1) da primeira linha deste exemplo n
ao foi
substitudo, uma vez que ele n
ao ocorre livre em (x . + ( x 1)) que e o corpo da -abstracao
mais externa.
2.8.5
Captura
(x . (y . yx))y (x . (z . zx))y z . zy
O mesmo problema pode ser vericado na -expressao (x . z . xz)(y . yz) que seria redutvel a (z . (y . yz)z) e o z de (y . yz) perderia seu contexto original, sendo capturado pelo
36
(y . + 5((x . x 3)y))6
+ 5 ((x . x 3)6)
+ 5 ( 6 3)
=+53
=8
2. Deve ser vericado que as funcoes embutidas podem ser construdas como quaisquer outras
funcoes. Por exemplo,
CON S = (a . b . f . f a b),
HEAD = (c . c(a . b . a)) e
T AIL = (c . c(a . b . b)).
Vejamos agora uma seq
uencia de reducoes envolvendo estas duas funcoes:
HEAD (CONS p q)
(CONS p q) (a . b . a)
= (a . b . f . f a b) p q ( a . b . a)
(b . f . f p b) q (a . b . a)
(f . f p q)(a . b . a)
(a . b . a) p q
(b . p) q
p
Isto implica que nao h
a a necessidade de que os construtores HEAD, CONS, TAIL ou uma
funcao qualquer sejam pre-denidas. Na realidade, eles existem apenas por quest
ao de eciencia.
2.9
Convers
ao, redu
c
ao e abstrac
ao
Podemos usar -reducao no sentido oposto, para encontrar uma nova -abstracao. Por exemplo,
+ 4 1 (x . + x 1)4. A isto chamamos de -abstracao. Uma -conversao e um termo generico
utilizado tanto para uma -reducao quanto para uma -abstracao, simbolizando-se por . Por
2. Aplica
c
ao de fun
c
ao: -conversao permite aplicar uma -abstracao a um argumento,
construindo uma nova inst
ancia do corpo da -abstracao.
3. Elimina
c
ao de -abstra
c
oes redundantes: -reducao pode, algumas vezes, eliminar
-abstracoes redundantes.
2.10
Provando a conversibilidade
Muito freq
uentemente, nos deparamos com casos nos quais temos que provar a conversibilidade
entre duas -abstracoes. Quando as duas -expressoes denotam a mesma funcao, o mecanismo
de prova pode se tornar muito complicado e tedioso. Por exemplo, sejam as -expressoes IF
TRUE ((p . p) 3) e (x . 3). Ambas denotam a mesma funcao, ou seja, a funcao que sempre
retorna o valor 3, independente dos valores dos argumentos reais. Assim, espera-se que elas
sejam conversveis uma na outra, j
a que denotam a mesma funcao. Realizando as -conversoes
sobre a primeira, temos:
IF TRUE ((p . p) 3)
IF TRUE 3
(x . IF TRUE 3 x)
= (x . 3)
(x . 3) w
pela def de IF
2.11
A aplicacao das regras de conversao nem sempre e tao simples e direta e, por isso mesmo,
sera mostrada uma denicao formal do que exatamente elas representam. Para isto, vamos
introduzir uma nova notacao que e bastante utilizada em exerccios de conversao de redexes, por
ser intuitiva para a implementacao de redutores computacionalmente.
A notacao E[M/x] signica que, na express
ao E, todas as ocorrencias livres de x serao
substitudas por M. Esta notacao nos permite expressar -conversoes bem mais formalmente,
sendo considerada mais natural por alguns implementadores.
Em uma aplicacao, a notacao se torna: (x . E) M E[M/x] que tambem pode ser utilizada
em -conversoes. A Tabela 2.3 mostra um resumo desta notacao.
38
-conversao: se y n
ao e livre em E, entao (x . E) (y . E[y/x])
-conversao: (x . E) M E[M/x]
-conversao: se x n
ao e livre em E e E denota uma funcao, ent
ao (x . E x) E
2.12
Ordem de reduc
ao
(x . x x) (x . x x) (x . x x) (x . x x) (x . x x) (x . x x) . . .
correspondendo a um loop innito nas linguagens imperativas.
2. Algumas seq
uencias de reducao podem atingir a sua forma normal enquanto outras n
ao.
Por exemplo, (x . 3) () pode ser avaliada para 3, usando-se o primeiro redex, mas, se
escolhermos o segundo ( aplicado a ), entraremos em loop innito.
39
(y . y)(z . z)
z . z
2.13
Fun
c
oes recursivas
para ele. Na realidade, os programas funcionais representam apenas uma forma mais adequada
de -expressoes. Dizemos que os programas funcionais sao acucaramentos sintaticos de expressoes do -calculo, para torn
a-lo mais adequado e mais f
acil de ser tratado.
No entanto existe um problema a ser resolvido que se refere ao fato de uma das caractersticas
not
aveis dos programas funcionais e a utilizacao massiva de funcoes recursivas [29] e estas funcoes
n
ao tem correspondentes no -calculo, conforme j
a foi visto ate aqui. Isto acontece porque, no
-calculo, as funcoes sao an
onimas e, portanto, n
ao podem ser chamadas recursivamente.
Assim, torna-se necessaria uma forma de se implementar funcoes recursivas no -calculo.
Nesta secao, vamos mostrar como estas funcoes sao traduzidas para o -calculo, sem a necessidade de qualquer extens
ao.
Voltemos, momentaneamente, nossa atencao para a denicao matematica da funcao f (x) =
Na realidade, estamos procurando valores para x e os respectivos resultados da aplicacao
da funcao f a estes valores. Como um caso particular, vamos procurar valores x que satisfacam
a igualdade f(x) = x, ou seja, estaremos procurando valores de x para os quais x3 x = x. Um
tal valor e x = 0, porque f (0) = 0. Mas x = 21/2 tambem sao outros valores que satisfazem a
igualdade f(x) = x.
x3 x.
Estes valores sao chamados de pontos xos e representam uma grande fonte de estudo matematico, destacando-se o teorema do ponto xo, alem de outros. Os pontos xos apresentam
caractersticas importantes, no entanto o nosso interesse aqui se prende exclusivamente em utiliz
a-los na construcao de funcoes recursivas no -calculo.
Considere agora a seguinte denicao recursiva da funcao fatorial:
FAT = (n . IF (= n 0) 1 (* n (FAT (- n 1)))).
Nesta denicao, damos um nome a uma -abstracao (FAT ) e nos referimos a ele mesmo,
dentro da -abstracao. Este tipo de construtor n
ao e provido pelo -calculo porque as abstracoes sao funcoes anonimas e, portanto, elas n
ao podem fazer referencias a nomes.
Vamos colocar a -expressao F AT em uma forma mais adequada ao nosso desenvolvimento.
Teremos entao FAT = n . (. . .FAT. . .), onde os pontos representam as outras partes da funcao que n
ao nos interessam, neste momento. Fazendo uma -abstracao em FAT,
transformamo-la em FAT = (fat . (n . (. . .fat. . .))) FAT.
Esta funcao pode ser escrita na forma FAT = H FAT onde, H = (fat . (n . (. . .fat. .
.))). Esta denicao de H e adequada aos nossos prop
ositos, uma vez que ela e uma -abstracao
ordin
aria e n
ao usa recursao. A equacao FAT = H FAT estabelece que quando a funcao H e
aplicada a FAT o resultado e o pr
oprio FAT. Ent
ao, FAT e um ponto xo de H.
Vamos agora procurar um ponto xo para H. Para isto vamos criar uma funcao, Y , que
toma H como argumento e retorna um ponto xo da funcao, como resultado. Assim Y deve ser
tal que Y H seja um ponto xo de H. Portanto, H(YH) = YH. Por este motivo, Y e chamado
de combinador de ponto xo. Se formos capazes de produzir tal combinador, nosso problema
estara resolvido.
Agora denimos FAT = Y H. Esta denicao n
ao e recursiva e atende a`s nossas necessidades.
Para vericar isto, vamos computar (FAT 1) utilizando as denicoes de FAT e de H, dadas
anteriormente e que o mecanismo de calculo obedece `a ordem normal de reducao ou seja, leftmostoutermost.
FAT = Y H
H = fat . n . IF (= n 0) 1 (* n(fat (- n 1))).
Ent
ao
41
FAT 1
= YH 1
= H (Y H) 1
= (fat . n . IF (= n 0) 1 (* n(fat (- n 1))) (Y H) 1
(n . IF (= n 0) 1 (* n ((Y H) (- n 1)))) 1
(* 1 (n . IF (= n 0) 1 (* n (Y H (- n 1)))) (- 1 1))
(* 1 (IF (= (- 1 1) 0) 1 (* (- 1 1) (Y H (- (- 1 1) 1)))))
= (* 1 (IF (= 0 0) 1 (* 0 (Y H (- 0 1)))))
= (* 1 (IF TRUE 1 (* 0 (Y H (- 0 1)))))
= (* 1 1)
pela denicao de IF
1
A forma como o combinador Y e denido j
a foi mostrada anteriormente, no entanto ele ser
a
novamente aqui denido, usando apenas algumas renomeacoes de vari
aveis:
Y = h . (x . h (x x)) (x . h (x x)).
Vamos agora avaliar Y H.
YH
= (h . (x . h (x x)) (x . h (x x))) H
(x . H (x x)) (x . H (x x))
2.14
Resumo
Este Captulo versou sobre os primeiros passos para quem necessita conhecer o -calculo de
forma simples e introdut
oria. Ele se fez necessario dada a import
ancia que esta teoria tem
na fundamentacao das linguagens funcionais. Seu papel e semelhante ao desempenhado pela
linguagem Assembly na traducao das linguagens imperativas para o c
odigo de maquina. No
entanto, deve ser salientado que esta teoria da matematica n
ao e tao simples como aqui parece.
Esta abordagem simples foi adotada, dado o car
ater introdut
orio exigido para se entender como
o -calculo e utilizado na compilacao de linguagens funcionais. Quem quizer se aprofundar neste
tema deve consultar a bibliograa indicada.
As notas de aula de Rafael Lins [22] e de Peter Welch [38] representam um bom comeco,
dada a grande quantidade de exerccios indicados e resolvidos. Quem estiver interessado em
detalhes mais aprofundados sobre a implementacao do -calculo deve consultar o livro de Peyton
Jones [29] que apresenta o -calculo de forma adequada para quem quer entender detalhes de
implementacao. Para uma abordagem mais te
orica desta linguagem, deve-se consultar os livros
de Bruce MacLennan [24] e de Antony Davie [7].
42
Captulo 3
Programa
c
ao funcional em Haskell
There are many distint pleasures
associated with Computer programming.
Craftsmanship has its quiet rewards, the satisfaction that
comes from building a useful object and making it work.
(Steven S. Skiena et Miguel A. Revilla in [33])
3.1
Introdu
c
ao
Os Captulos anteriores foram feitos com o objetivo de servirem como preparacao e fundamentacao para o estudo das linguagens funcionais, em particular, de Haskell. Este Captulo e os
seguintes sao todos dedicados `a codicacao de programas funcionais utilizando esta linguagem.
A comunidade das linguagens funcionais tem dado a Haskell uma atencao especial e, por este
motivo, muita pesquisa tem sido feita tentando dot
a-la de caractersticas que a torne uma linguagem de uso popular. Estas caractersticas foram citadas por Philip Wadler [37] e analisadas
na Introducao desta Apostila.
Haskell e uma linguagem funcional pura, n
ao estrita, fortemente tipada, cujo nome e uma
homenagem a Haskell Brooks Curry, um estudioso da logica combinatorial e um dos mais pro uma linguagem baseada em scripts, que
eminentes pesquisadores sobre -calculo [4, 35]. E
consistem em um conjunto de denicoes associadas a nomes, em um arquivo.
Em 1998, a comunidade de Haskell padronizou Haskell98 como a versao a ser utilizada ate
a denicao de Standard Haskell. No entanto, a linguagem continua sendo pesquisada buscando
a criacao de novas Bibliotecas a serem incorporadas ao sistema. Tambem muitas extensoes
estao sendo incorporadas, como Haskell paralelo, IDLs (Interface Description Language), por
exemplo HaskellDirect e interfaces para C e C++, permitindo integracao com estas e outras
linguagens. Em particular, tem sido desenvolvida AspectH, uma extensao de Haskell para
suportar orientacao a aspectos [2], alem de uma extensao para OpenGL.
O site ocial na WEB sobre Haskell e: http://www.haskell.org, onde todas as informacoes
sobre ela podem ser encontradas, alem de varios links para compiladores e interpretadores para
a linguagem.
O interpretador mais popular de Haskell e Hugs, desenvolvido por Mark Jones na Universidade de Nottingham e da Universidade de Yale. Esta implementacao, codicada em C,
e pequena, f
acil de ser usada e disponvel para v
arias plataformas, incluindo UNIX, Linux,
Windows 3.x, Win32, DOS e ambiente Macintosh.
Para produzir c
odigo executavel de maquina, foram desenvolvidos v
arios compiladores. Na
Universidade de Glasgow, foi construdo GHC (Glasgow Haskell Compiler) disponvel para
43
considerado
ambientes UNIX (Linux, Solaris, *BSD e MacOS-X) e tambem para Windows. E
um pouco lento e necessita de muita memoria. Est
a disponvel de forma livre em
http://www.dcs.gla.ac.uk/fp/software/ghc/
Na Universidade de Chalmers, foram desenvolvidos o interpretador HBI (Haskell-B Interpreter) e o compilador HBC (Haskell-B Compiler) disponveis em
http://www.cs.chalmers.se/ augustss/hbc.html.
Tambem esta disponvel o compilador nhc98, considerado f
acil de ser instalado, com heap
profiles, muito menor que os outros compiladores, disponvel para todos os padr
oes UNIX e
escrito em Haskell 98.
3.2
Primeiros passos
Existem duas formas nas quais um texto e considerado um programa em Haskell. A primeira
delas e considerar todo o texto como um programa, exceto o que e comentado, que pode ser
de duas maneiras: com - -, que representa um comentario ate o nal da linha corrente, ou
envolvendo o coment
ario com os smbolos {- e -}, podendo englobar v
arias linhas. Os
arquivos em Haskell com este tipo de programa devem ter a extensao .hs. Esta e a maneira mais
utilizada pelos programadores de Haskell.
A outra forma e considerar todo o texto como comentario e sinalizar o que deve ser realmente
um programa iniciando a linha com o sinal > identado. Neste caso, o arquivo deve ter extensao
.lhs. Vamos mostrar um exemplo de cada situacao.
{- ######################################################################
exemplo.hs
Este arquivo eh um exemplo de um arquivo .hs. Deve ser editado como arquivo
texto e salvo com a extensao .hs.
#########################################################################-}
resposta :: Int -- Uma constante inteira
resposta = 42
novalinha :: Char
novalinha = \n
sim :: Bool
sim = True
maior :: Bool
maior = (resposta > 71)
quadrado :: Int -> Int
quadrado x = x*x
todosIguais :: Int -> Int -> Int -> Bool
todosIguais n m p = (n ==m) && (m ==p)
{-######################################################################-}
Agora vamos mostrar o mesmo exemplo usando a visao de literal, com a extensao .lhs. Deve
44
3.2.1
O interpretador Hugs
Comando
:?
:e
:e exemplo.hs
:l exemplo.hs
:a exemplo.hs
:q
Para o usu
ario executar qualquer funcao do Prelude e necessario apenas chamar esta funcao
na linha de comandos (prompt), disponvel ap
os o interpretador ser carregado, seguida de seus
argumentos e apertar a tecla Enter. O resultado sera exibido imediatamente na linha seguinte
ap
os o prompt. O interpretador tambem pode ser usado como uma calculadora para avaliar
expressoes aritmeticas, booleanas, logartmicas, trigonometricas, etc. A forma de utilizacao e
45
apenas colocar a expressao no prompt e apertar Enter. Caso a funcao ou expressao a ser
executada esteja com seus parametros colocados corretamente, o resultado aparecera na linha
seguinte. Caso a chamada da funcao ou expressao n
ao esteja correta, sera exibida uma mensagem
de erro. Algumas destas funcoes ou operadores aritmeticos estao mostrados na Tabela 3.2.
:? 2 + 3 <enter>
5
:? (1 * 6) == (3 div 5) <enter>
False
:? sin(3 + 4) <enter>
Error: Type mismatched
3.2.2
Identificadores em Haskell
3.3
else
hiding
if
import
in
infix
infixl
infixr
instance
let
module
of
renaming
then
to
type
where
Fun
c
oes em Haskell
As formas de denicao de funcoes em Haskell tem a ver com as formas de denicao de funcoes
utilizadas na Matematica, mostradas no Captulo 1. Em Haskell, elas podem ser pensadas e
representadas gracamente por uma caixa que recebe um ou mais parametros como entrada
(argumentos), processa-os e constroi um resultado u
nico que e exibido como sada, conforme
pode ser visto na Figura 3.1.
Int
Int
Quadro
Int
escala
Int
Quadro
espelhaV
espelhaH
invertecor
escala
sobrepoe
3.3.1
Construindo fun
c
oes
Um tipo de dado e uma colecao de valores onde todos eles tem as mesmas caractersticas.
Por exemplo, os n
umeros inteiros, os caracteres, os strings de caracteres, etc. As funcoes em
Haskell podem ser declaradas com o seu nome, seguido de ::, vindo em seguida os tipos de seus
argumentos, um a um, com uma echa ( >) entre eles e, nalmente mais uma echa seguida
do tipo do resultado que a aplicacao da funcao produz. Por exemplo, as funcoes + e escala,
mostradas gracamente na Figura 3.1, tem seus tipos denidos da forma a seguir:
+ :: Int -> Int -> Int e
escala :: Quadro -> Int -> Quadro
O tipo da funcao e daclarado em sua forma curricada, onde uma funcao de n argumentos
nico argumento, da mesma forma adotada pelo -calculo,
e considerada como n funcoes de um u
vista no Captulo 2.
Os tipos nos d
ao informacoes importantes sobre a aplicacao das funcoes. Por exemplo, a
declaracao da funcao escala, mostrada anteriormente, nos informa que:
a funcao escala tem dois argumentos de entrada, sendo o primeiro do tipo Quadro e o
segundo do tipo Int e
o resultado da aplicacao da funcao e um valor do tipo Quadro, facilitando o entendimento
do problema e a forma de solucao adotada para resolve-lo.
Alem dos tipos, a declaracao de uma funcao deve exibir explicitamente como o processamento
de seus argumentos deve ser feito ou pode declarar que este processamento seja feito atraves
de denicoes ja feitas para outras funcoes, sendo esta uma forma muito comum de declaracao.
Como exemplo do primeiro caso, podemos vericar a denicao da funcao somaAmbos. A
forma como ela processa um par de valores, (x, y), e somando seus valores, x + y.
Como exemplo de declaracao de uma funcao atraves de outras funcoes, temos a funcao
rotaciona, que promove uma rotacao em torno da origem do sistema de coordenadas cartesianas
x0y, pode ser denida em funcao das funcoes espelhaV e espelhaH, denidas anteriormente.
rotaciona :: Quadro -> Quadro
rotaciona pic = espelhaH (espelhaV pic)
Algumas linguagens funcionais exigem que os tipos das funcoes sejam declarados explicitamente pelo programador. Em Haskell, seus projetistas optaram por deixar que os tipos das
funcoes sejam inferidos pelo sistema. Isto signica que e opcional a declaracao dos tipos das
funcoes pelo programador. Mesmo assim, encoraja-se que esta declaracao seja feita explicitamente, como forma de disciplina de programacao. Isto permite ao programador um completo
entendimento do problema e da solucao adotada.
Voltando ao exemplo da funcao rotaciona, ela tambem pode ser codicada de outra forma,
mais elegante, utilizando a composicao de funcoes, uma propriedade implementada apenas nas
linguagens funcionais. Esta forma de construcao de funcoes sera vista com detalhes ainda neste
Captulo, no entanto, ela pode ser feita em Haskell usando a notacao de ponto (.), a mesma
adotada para a composicao de funcoes na Matem
atica.
rotaciona
espelhaH . espelha
49
espelhaH cavalo
......##....
.....#.#....
....#..#....
...#...#....
..#...#.....
..#...#..##.
..#...###.#.
..#...#...#.
..#.......#.
...##.....#.
.....##..#..
.......##...
espelhaV cavalo
...##.......
..#..##.....
.#.....##...
.#.......#..
.#...#...#..
.#.###...#..
.##..#...#..
.....#...#..
....#...#...
....#..#....
....#.#.....
....##......
O tipo lista e um tipo primitivo em Haskell (o tipo mais importante nas linguagens funcionais) e, dada a sua
import
ancia, ser
a dedicado, um Captulo completo (o Captulo 4) sobre a sua construca
o e utilizaca
o.
50
Um argumento da funcao map e reverse e reverse e tambem uma funcao. Isto signica
que uma funcao (reverse) pode ser passada como par
ametro para uma outra funcao.
Neste caso, diz-se que as func
oes s
ao cidad
aos de primeira categoria, permitindo a elas os
mesmos direitos que qualquer outro tipo de dado.
O resultado da aplicacao da funcao map `a funcao reverse e uma outra funcao que e
aplicada a um outro argumento que, neste caso, e uma lista. Neste caso, diz-se que, nas
linguagens funcionais, as func
oes s
ao de alta ordem, ou seja, podem ser passadas como
argumentos de uma funcao e tambem podem retornar como resultados da aplicacao de
uma funcao.
3.3.2
Avalia
c
ao de func
oes em Haskell
3.3.3
Casamento de padr
oes (patterns matching)
O casamento de padr
oes e outra forma de codicacao de funcoes em Haskell, baseada na denicao por enumeracao utilizada na Matematica, vista no Captulo 1. Neste tipo de denicao,
sao exibidos todos os valores que os argumentos da funcao podem ter e, para cada um deles,
declara-se o valor do resultado correspondente. De forma resumida, exibem-se todos pares do
mapeamento (entrada, resultado). Por exemplo, vejamos as declaracoes das funcoes eZero e
fat, a seguir:
eZero :: Int -> Bool
eZero 0 = True
eZero _ = False
funcao para o caso de n ser 0 for trocada pela segunda denicao, o resultado da aplicacao da
funcao sera sempre igual a False, mesmo que o argumento seja 0, uma vez que o (undescore)
signica qualquer caso.
Exerccios:
1. De a denicao da funcao todosQuatroIguais do tipo Int >Int >Int >Int >Bool
que d
a o resultado True se seus quatro argumentos forem iguais.
2. De denicao da funcao todosQuatroIguais usando a denicao da funcao todosIguais,
dada anteriormente.
3. O que esta errado com a denicao da funcao todosDiferentes abaixo?
todosDiferentes n m p = ( (n /= m) & & (m /= p) )
4. Projete um teste adequado para a funcao todosIguais, considerando a funcao
teste :: Int >Int >Int >Int
teste n m p = ((n+m+p) == 3*p).
Esta funcao se comporta da mesma forma que a funcao todosIguais para o seu teste de
dados? Que conclus
ao voce tira sobre os testes em geral?
5. De uma denicao para a funcao quantosIguais usando as funcoes todosIguais e todosDiferentes.
6. Escreva a sequencia de calculos para as seguintes express
oes:
maximo ((2 + 3) 7) (4 + (1 3))
todosIguais 4 quadrado 2 3
quantosIguais 3 4 3
3.4
Haskell, a exemplo de qualquer linguagem de programacao, prove uma colecao de tipos primitivos
e tambem permite tipos estruturados, denidos pelo programador, provendo grande exibilidade
na modelagem de programas.
3.4.1
Os tipos primitivos de Haskell sao: o tipo inteiro (Int ou Integer), o tipo booleano (Bool), o
tipo caractere (Char), o tipo cadeia de caracteres (String) e o tipo ponto utuante (Float ou
Double) e o tipo lista. Nesta secao, vamos analisar cada um destes tipos primitivos, deixando
o tipo lista para ser tratado no Captulo 4, dada a sua import
ancia nas linguagens funcionais e,
em particular, em Haskell. Tambem serao estudados os tipos estruturados possveis em Haskell.
O tipo inteiro (Int ou Integer)
Como em muitas linguagens, o tipo inteiro e primitivo em Haskell. Seu domnio de valores
e o mesmo das outras linguagens. Os valores do tipo Integer sao representados com o dobro
da quantidade de bits necess
arios para representar os valores do tipo Int. Seus operadores
aritmeticos sao os mesmos admitidos na maioria das outras linguagens:
Operadores aritm
eticos para valores dos tipos Int ou Integer.
52
+, *
div
mod
abs
negate
adicao e multiplicacao
exponenciacao
subtracao (inxa) e inversor de sinal (prexa)
divis
ao inteira (prexa), ou div (inxa)
modulo (prexa), ou mod (inxa)
valor absoluto de um inteiro
troca o sinal de um inteiro
Os operadores relacionais tambem sao os mesmos encontrados na grande maioria das linguagens.
Operadores relacionais: Int >Int >Bool
>, >=, ==, / =, <=, <
Vejamos um exemplo simples de construcao de funcao usando inteiros.
mdc :: Int -> Int -> Int
mdc n m
|m == n
= n
|m > n
= mdc m n
|otherwise = mdc (n - m) m
Exemplo. Vamos agora mostrar uma forma de construcao de um programa que envolve
operacoes com inteiros. Seja uma empresa que necessita de respostas para as questoes a seguir, para fundamentar suas tomadas de decis
oes:
Questao 1: Qual o total de vendas desde a semana 0 ate a semana n?
Questao 2: Qual a maior venda semanal entre as semanas 0 e n?
Questao 3: Em que semana ocorreu a maior venda?
Questao 4: Existe alguma semana na qual nada foi vendido?
Questao 5: Em qual semana n
ao houve vendas? (se houve alguma).
Vamos construir algumas funcoes em Haskell para responder a algumas destas questoes,
deixando algumas outras para exerccio do leitor. Para isto e necessario que recordemos as
denicoes matematicas de funcoes, vistas no Captulo 1.
Quest
ao 1:
Para solucionar a Quest
ao 1, devemos inicialmente construir uma funcao vendas i que
vai nos informar qual o valor das vendas na semana i. Esta funcao pode ser construda de
v
arias formas, no entanto ser
a feita aqui uma denicao por casamento de padr
oes, semelhante
`a denicao por enumeracao da Matem
atica.
vendas
vendas
vendas
vendas
Veriquemos agora como as denicoes recursivas utilizadas na Matematica podem ser utilizadas em Haskell. Recordemos a denicao matematica de uma funcao usando recurs
ao. Por
exemplo, na denicao de uma funcao fun, devemos:
53
totaldeVendas 1 + vendas 2
(totaldeVendas 0 + vendas 1) + vendas 2
(vendas 0 + vendas 1) + vendas 2
(7 + vendas 1) + vendas 2
(7 + 2) + vendas 2
9 + vendas 2
9 + 5
14
Quest
ao 2:
Vamos agora denir a funcao maxVendas, onde maxVendas i sera igual a vendas i,
se a venda maxima ocorreu na semana i. Se a maior venda ocorreu na semana 0, a funcao
maxVendas 0 sera vendas 0. Se a maior venda ocorreu ate a semana n, pode estar ate a
semana (n-1) ou sera vendas n.
maxVendas :: Int -> Int
maxVendas n
|n == 0
|maxVendas (n-1) >= vendas n
|otherwise
= vendas 0
= maxVendas (n - 1)
= vendas n
Esta mesma funcao pode ser construda de outra forma, usando uma funcao auxiliar, maximo, que aplicada a dois inteiros retorna o maior entre eles.
maximo :: Int -> Int -> Int
maximo x y
54
|x >= y
= x
|otherwise = y
maxVendas n
|n == 0
|otherwise
= vendas 0
= maximo (maxVendas (n - 1)) vendas n
= n (equacao condicional)
=m
Nome
and
or
inversor
Tipo
&& :: Bool > Bool > Bool
|| :: Bool > Bool > Bool
not :: Bool > Bool
\ - aspas simples
\ - aspas duplas
\34 - ?
4. Dena uma funcao romanoParaString :: Char > String que converte um algarismo
romano em sua representacao em Portugues. Por exemplo, romanoParaString V =
cinco.
5. Dena uma funcao emTresLinhas :: String > String > String > String que
toma tres strings e retorna um u
nico string mostrando os tres strings em linhas separadas.
6. Dena uma funcao replica :: String > Int > String que toma um String e um
n
umero natural n e retorna n copias da String, todas juntas. Se n for 0, o resultado deve
ser a String vazia (), se n for 1, retorna a pr
opria String.
O tipo ponto flutuante (Float ou Double)
Os valores do tipo ponto utuante (n
umeros reais) pertencem aos tipos Float ou Double,
da mesma forma que um n
umero inteiro pertence aos tipos Int ou Integer. Isto signica que
as u
nicas diferencas entre valores destes tipos se vericam na quantidade de bits usados para
represent
a-los. A Tabela 3.3 mostra as principais funcoes aritmeticas pre-denidas na linguagem.
As funcoes aritmeticas recebem a denominacao especial de operadores.
Tabela 3.3: Operadores aritmeticos de ponto utuante em Haskell.
+, - *
Float > Float > Float
/
Float > Float > Float
Apesar de alguns pesquisadores, mais puristas, condenarem a sobrecarga de operadores, alguns outros defendem que alguma forma de sobrecarga deve existir, para facilitar a codicacao
de programas. Os projetistas de Haskell adimitiram a sobrecarga de operadores para ser utilizada na implementacao de uma de suas caractersticas importantes e que permite um grau de
abstracao bem maior que normalmente se encontra em outras linguagens. Esta caracterstica se
refere `as classes de tipos (type class), um tema a ser analisado no Captulo 5.
Exerccio
Dena uma funcao mediadasVendas :: Int > Float onde mediadasVendas n e a
media aritmetica entre os valores de vendas 0 ate vendas n.
58
3.4.2
Programando com n
umeros e strings
3.4.3
Apesar dos resultados das aplicacoes das funcoes somaPar e somaDois serem os mesmos,
elas sao distintas. A funcao somaPar requer apenas um argumento, neste caso uma tupla,
enquanto a funcao somaDois requer dois argumentos do tipo inteiro.
3.4.4
Escopo
O escopo de uma denicao e a parte de um programa na qual ela e visvel e portanto pode ser
usada. Em Haskell, o escopo das denicoes e todo o script, ou seja, todo o arquivo no qual a
denicao foi feita. Por exemplo, vejamos a denicao de ehImpar n, a seguir, que menciona a
funcao ehPar, apesar desta ser denida depois. Isto so e possvel porque elas compartilham o
mesmo escopo.
ehImpar, ehPar :: Int -> Bool
ehImpar 0 = False
ehImpar n = ehPar (n-1)
ehPar 0 = True
ehPar n = ehImpar (n-1)
Defini
c
oes locais
Haskell permite denicoes locais atraves da palavra reservada where. Por exemplo,
somaQuadrados :: Int -> Int -> Int
somaQuadrados n m = quadN + quadM
where
quadN = n * n
quadM = m * m
As denicoes locais podem incluir outras denicoes de funcoes, alem de poder usar denicoes
locais a uma express
ao, usando a palavra reservada let.
61
let x = 3 + 2; y = 5 - 1 in x2 + 2*x*y - y
As denicoes locais sao visveis apenas na equacao onde elas foram declaradas. As vari
aveis
que aparecem do lado esquerdo da igualdade tambem podem ser usadas em denicoes locais, do
lado esquerdo. Por exemplo,
maximoQuadrado x y
|quadx > quady = quadx
|otherwise = quady
where
quadx
quady
quad
quad z
= quad x
= quad y
:: Int -> Int
= z * z
As denicoes locais podem ser usadas antes delas serem denidas e tambem podem ser usadas
em resultados, em guardas ou em outras denicoes locais. Como exemplo,
maximasOcorrencias :: Int -> Int -> Int -> (Int, Int)
maximasOcorrencias n m p = (max, quantosIguais)
where
max
= maximoDeTres n m p
quantosIguais
= quantosIguaisValor
maximoDeTres
:: Int -> Int -> Int
maximoDeTres a b c = maximo (maximo (a,
quantosIguaisValor :: Int -> Int -> Int
max n m p
-> Int
b), c)
-> Int -> Int
onde a funcao quantosIguaisValor pode ser denida de uma das formas mostradas na Tabela
a seguir.
quantosIguaisValor valor n m p
= ehN + ehM + ehP
where
ehN = if n == valor then 1 else 0
ehM = if m == valor then 1 else 0
ehP = if p == valor then 1 else 0
quantosIguaisValor valor n m p
= ehvalor n + ehvalor m + ehvalor p
where
ehvalor :: Int > Int
ehvalor x = if x == valor then 1 else 0
3.4.5
C
alculos:
3. Dena uma funcao stars :: Int > String de forma que stars 3 retorna ***. Como
deve ser tratada uma entrada negativa?
3.4.6
Projeto de programas
3.4.7
Provas de programas
Existem em Haskell tres maneiras de realizar provas: a prova direta, a prova por casos e a
prova por inducao matematica. Vamos analisa-las atraves de exemplos, lembrando ao leitor que
o domnio dessas tecnicas so e conseguido atraves da pr
atica.
Provas diretas
A prova direta e feita aplicando as denicoes das funcoes. Por exemplo, sejam as funcoes troca,
cicla e recicla, denidas da seguinte forma:
troca :: (Int, Int) -> (Int, Int)
troca (a, b) = (b, a)
--def 1
= troca (b, a)
= (a, b)
--por def 1
--por def 1
-- (fat 1)
-- (fat 2)
Est
agio 3:
Est
agio 4:
(1)
(2)
(3)
(4)
Est
agio 3:
Exerccios
1. Prove que, para todo n
umero natural n, fatorial (n + 1) power2 n.
2. Prove que, para todo n
umero natural n, fib (n+1) power2 (n div 2).
68
3.5
Resumo
Neste Captulo, foi dado incio ao estudo de programacao em Haskell. Foram vistos os tipos de
dados primitivos e as tuplas. Alguns exerccios foram resolvidos para dar uma nocao ao usu
ario
da potencialidade da linguagem e outros foram deixados para o leitor.
O livro de Simon Thompson [35] foi a fonte mais utilizada para o estudo mostrado no
Captulo, por ser um livro que apresenta um conte
udo te
orico bem estruturado e fundamentado,
alem de muitos exerccios resolvidos e muitos problemas propostos.
O livro de Richard Bird [4] e outra fonte importante de exerccios resolvidos e propostos,
apesar de sua seq
uencia de abordagens seguir uma ordem distinta, exigindo um conhecimento
anterior sobre programacao funcional, o que o torna mais defcil de ser seguido por iniciantes
neste tema.
Outra referencia importante e o livro de Paul Hudak [14] que apresenta a programacao
funcional em Haskell com exemplos aplicados `a Multimdia, envolvendo a construcao de um
editor gr
aco e de um sistema utilizado em m
usica, entre outros. Para quem imagina que
Haskell s
o e aplicado a problemas da Matematica, esta referencia poe por terra este argumento.
69
70
Captulo 4
O tipo Lista
Commputers specially designed for applicative languages
implement recursive functions very eciently.
Also, architectural features can be included in conventional
computers that signicantly increase
the speed of recursive-function invocations.
(Bruce J. MacLennan in [24])
Lista e o tipo de dado mais importante nas linguagens funcionais. Todas as linguagens
funcionais a implementam como um tipo primitivo, juntamente com uma gama imensa de funcoes
para a sua manipulacao. A notacao utilizada para as listas e colocar seus elementos entre
colchetes. Por exemplo, [1,2,3,4,1,3] e uma lista de inteiros, [True,False] e uma lista de
booleanos, [a, a, b] e uma lista de caracteres e [Marta, Marlene] e uma lista de
strings. H
a, no entanto, que se diferenciar as listas homogeneas, que s
ao as listas onde todos
os valores sao do mesmo tipo, das listas heterogeneas, onde os componentes podem ter mais de
um tipo. Haskell s
o admite listas homogeneas. Por exemplo, [False, 2,Maria] n
ao e uma
lista em Haskell, por ser heterogenea. Em compensacao, podemos ter a lista [totalVendas,
totalVendas], que tem o tipo [Int > Int] como tambem a lista [[12,1], [3,4], [4,4,4,4,4],
[ ]] que tem o tipo [[Int]], uma lista de listas de inteiros, alem de outras possibilidades.
Existem duas formas como as listas podem se apresentar:
a lista vazia, simbolizada por [ ], que pode ser de qualquer tipo. Por exemplo, ela pode
ser de inteiros, de booleanos, etc. Dito de outra forma, [ ] e do tipo [Int] ou [Bool] ou
[Int > Int], signicando que a lista vazia est
a na intersecao de todas as listas, sendo o
u
nico elemento deste conjunto.
a lista n
ao vazia, simbolizada por (a : x), onde a representa um elemento da lista,
portanto tem um tipo, e x representa uma lista composta de elementos do mesmo tipo de
a. O elemento a e chamado de cabe
ca e x e a cauda da lista.
Algumas caractersticas importantes das listas em Haskell, sao:
A ordem em uma lista e mportante, ou seja, [1,3] /= [3,1] e [False] /= [False, False].
A lista [n .. m] e igual a` lista [n, n+1, ..., m]. Por exemplo, [1 .. 5] = [1, 2, 3, 4, 5].
A lista [3.1 .. 7.0] = [3.1, 4.1, 5.1, 6.1].
A lista [n,p .. m] e igual a` lista de n ate m em passos de p-n. Por exemplo, [7,6 ..3] =
[7, 6, 5, 4, 3] e [0.0, 0.3 ..1.0] = [0.0, 0.3, 0.6, 0.9].
71
4.1
Fun
c
oes sobre listas
As funcoes para a manipulacao de listas sao declaradas da mesma forma como sao declaradas
para processar outros tipos de dados, usando casamento de padr
oes. Neste caso, os padroes sao
apenas dois: a lista vazia e a lista nao vazia. Por exemplo,
somaLista :: [Int] -> Int
somaLista [ ] = 0
somaLista (a:x) = a + somaLista x
A funcao somaLista toma como argumento uma lista de inteiros e retorna, como resultado,
um valor que e a soma de todos os elementos da lista argumento. Se a lista for vazia ([ ]), a
soma sera 0. Se a lista nao for vazia (a : x), o resultado ser
a a soma de sua cabeca (a) com
o resultado da aplicacao da mesma funcao somaLista `a cauda da lista (x). Esta denicao
e recursiva, uma caracterstica muito utilizada, por ser a forma usada para fazer iteracao em
programas funcionais. Devemos observar que a ordem em que os padroes sao colocados tem
import
ancia fundamental. No caso em voga, primeiramente foi feita a denicao para a lista
vazia e depois para a lista n
ao vazia. Dependendo da necessidade do programador, esta ordem
pode ser invertida.
Vejamos a seq
uencia de calculos da aplicacao da funcao somaLista `a lista [2, 3, 5, 7].
somaLista [2,3,5,7] =2 +
=2 +
=2 +
=2 +
=2 +
=2 +
=2 +
=2 +
=17
4.1.1
somaLista [3,5,7]
(3 + somaLista [5,7])
(3 + (5 + somaLista [7]))
(3 + (5 + (7 + somaLista [])))
(3 + (5 + (7 + 0)))
(3 + (5 + 7))
(3 + 12)
15
O construtor de listas, chamado de cons e sinalizado por : (dois pontos), tem import
ancia fundamental na construcao de listas. Ele e um operador que toma como argumentos um elemento,
de um tipo, e uma lista de elementos deste mesmo tipo e insere este elemento como a cabeca da
nova lista. Por exemplo,
10 : [ ] = [10]
2 : 1 : 3 : [ ] = 2 : 1 : [3] = 2 : [1,3] = [2,1,3]
signicando que cons (:) associa seus componentes pela direita, ou seja:
72
a : b : c = a : (b : c) /= (a : b) : c
Mas qual o tipo de cons? Observando que 4 : [3] = [4, 3], ent
ao cons tem o tipo:
(:) :: Int -> [Int] -> [Int]
No entanto, vericamos tambem que True : [False] = [True, False]. Agora cons tem o
tipo:
(:) :: Bool -> [Bool] -> [Bool]
Isto mostra que o operador cons e polim
orco. Desta forma, seu tipo e:
(:) :: t -> [t] -> [t]
onde [t] e uma lista de valores, de qualquer tipo, desde que seja homogenea.
4.1.2
Construindo fun
c
oes sobre listas
Em Haskell, j
a existe um grande n
umero de funcoes pre-denidas para a manipulacao de listas.
Estas funcoes fazem parte do arquivo Prelude.hs, carregado no momento em que o sistema e
chamado e permanece ativo ate o nal da execucao. Um resumo destas funcoes, com seus tipos
e exemplos de utilizacao, pode ser visto na Tabela 4.1.
Tabela 4.1: Algumas funcoes polim
orcas do Prelude.hs.
Funcao
Tipo
Exemplo
:
a > [a] > [a]
3:[2,5]=[3,2,5]
++
[a] > [a] > [a]
[3,2]++[4,5]=[3,2,4,5]
!!
[a] > Int > a
[3,2,1]!!1=3
concat
[[a]] > [a]
[[2],[3,5]]=[2,3,5]
length
[a] > Int
length [3,2,1]=3
head
[a] > a
head [3,2,5]=3
last
[a] > a
last [3,2,1]=1
tail
[a] > [a]
tail [3,2,1]=[2,1]
init
[a] > [a]
init [3,2,1]=[3,2]
replicate Int > a > [a]
replicate 3 a=[a,a,a]
take
Int > [a] > [a]
take 2 [3,2,1]=[3,2]
drop
Int > [a] > [a]
drop 2 [3,2,1]=[1]
splitAt
Int > [a] > ([a], [a]) splitAt 2 [3,2,1]=([3,2],[1])
reverse
[a] > [a]
reverse [3,2,1]=[1,2,3]
zip
[a] > [b] > [(a, b)]
zip[3,2,1][5,6]=[(3,5),(2,6)]
unzip
[(a, b)] > ([a], [b])
unzip [(3,5),(2,6)]=([3,2],[5,6])
and
[Bool] > Bool
and [True,False]=False
or
[Bool] > Bool
or [True,False]=True
sum
[Int] > Int
sum [2,5,7]=14
[F loat] > F loat
sum [3.0,4.0,1.0]=8.0
product [Int] > Int
product [1,2,3]=6
[F loat] > F loat
product [1.0,2.0,3.0]=6.0
73
A lista vazia e considerada ordenada por vacuidade. Por isto a primeira denicao. Para o
caso da lista n
ao vazia, devemos inserir a cabeca (a) na cauda j
a ordenada (ordena x). Falta
apenas denir a funcao insere, que e auto-explicativa.
insere :: Int -> [Int] -> [Int]
insere a [ ] = [a]
insere a (b:y)
|a <= b = a : (b : y)
-- a serah a cabeca da lista
|otherwise = b : insere a y -- procura colocar a no local correto
Este metodo de ordenacao e conhecido como inserc
ao direta e o leitor deve observar a
simplicidade como ele e implementado em Haskell. Sugerimos comparar esta implementacao
com outra, em qualquer linguagem convencional. Alem disso, tambem se deve considerar a
possibilidade de aplicacao desta mesma denicao a` listas de varios tipos de dados, signicando
que a denicao pode ser polim
orca. Isto signica que, na denicao da funcao insere, a u
nica
operacao exigida sobre os valores dos elementos da lista a ser ordenada e que eles possam ser
comparados atraves da operacao . Uma lista de valores, de qualquer tipo de dados, onde esta
operacao seja possvel entre estes valores, pode ser ordenada usando esta denicao.
Exerccios
1. Mostre todos os passos realizados na chamada ordena [2, 8, 1].
2. Dena uma funcao numOcorre :: [t] > t > Int, onde numOcorre l s retorna o
n
umero de vezes que o tem s aparece na lista l.
3. De uma denicao dia funcao fazParte usando a funcao numOcorre, do tem anterior.
4. Dena uma funcao unico :: [Int] > [Int] que retorna a lista de n
umeros que ocorrem
exatamente uma vez em uma lista. Por exemplo, unico [2,4,2,1,4] = [1].
4.2
O casamento de padr
oes ja foi analisado anteriormente, mas sem qualquer profundidade e formalismo. Agora ele sera visto com uma nova roupagem, apesar de usar conceitos j
a conhecidos.
Os padr
oes em Haskell sao dados por:
valores literais como -2, C e True.
vari
aveis como x, num e maria.
o caractere
ao tem de ser
um padr
ao de tuplas (p1 , p2 , ..., pk ). Um argumento para casar com este padr
da forma (v1 , v2 , ..., vk ), onde cada vi deve ser do tipo pi .
um construtor aplicado a outros padr
oes. Por exemplo, o construtor de listas (p1 : p2 ),
oes
onde p1 e p2 sao padr
Nas linguagens funcionais, a forma de vericar se um padr
ao casa, ou nao, com um dado e
realizada da seguinte maneira:
primeiramente, analisa-se se o argumento esta na forma correta e
75
Que motivos o leitor imagina que o programador tenha levado em conta na denicao da
funcao devolveLivro, a exemplo da funcao zip, denida anteriormente, preferindo apresentar
a denicao para o padr
ao de lista vazia ap
os a denicao para o padr
ao de lista n
ao vazia, quando
o normal seria apresentar estes padroes na ordem inversa?
Exerccio:
Modique o banco de dados da Biblioteca anterior e as funcoes de acesso, de forma que:
exista um n
umero maximo de livros que uma pessoa possa tomar emprestado,
exista uma lista de palavras-chave associadas a cada livro, de forma que cada livro possa
ser encontrado atraves das palavras-chave a ele associadas, e
existam datas associadas aos emprestimos, para poder detectar os livros com datas de
emprestimos vencidas.
4.3
Compreens
oes e express
oes ZF (Zermelo-Fraenkel)
As compreens
oes, tambem conhecidas como expressoes ZF, sao devidas a Zermelo e Fraenkel e
representam uma forma muito rica de construcao de listas. O domnio desta tecnica permite
ao programador resolver muitos problemas de maneira simples e, em muitos casos, inusitada.
A sintaxe das expressoes ZF e muito pr
oxima da descricao matematica de conjuntos por intensionalidade, exprimindo determinadas propriedades. As diferencas se vericam apenas nos
sinais utilizados nas representacoes, mas a logica subjacente e a mesma. Vamos mostrar estas
semelhancas atraves de exemplos e depois vamos formalizar sua sintaxe.
Vamos supor que ex = [2,4,7]. Usando ex, podemos construir ex1, a lista cujos elementos
sejam o dobro dos elementos de ex, da seguinte forma:
ex1 = [2*a | a<-ex]
Desta forma ex1 = [4,8,14]. Se quizermos encontrar a lista ex2 composta dos elementos de
ex que sejam pares, podemos declarar
ex2 = [a | a<-ex, a mod 2 == 0]
Neste caso, ex2 = [2,4].
A partir destes exemplos, podemos vericar que a sintaxe das express
oes ZF e realmente
simples. Formalmente ela e dada da seguinte forma:
[ e | q1 , ..., qk ] onde cada qi e um qualicador, que pode ter umas das seguintes formas:
1. pode ser um gerador do tipo p< lExp, onde p e um padr
ao e lExp e uma expressao do
tipo lista, ou
2. pode ser um teste do tipo bExp, uma expressao booleana.
Propriedades de uma express
ao ZF
Os geradores podem ser combinados com nenhuma, uma ou mais express
oes booleanas.
Sendo ex a lista do Exemplo anterior, ent
ao
[2*a | a <- ex, a mod 2 == 0, a > 3] = [8]
78
Esta denicao pode tambem ser feita usando denicoes locais, tornando-a mais f
acil de ser
compreendida, da seguinte forma.
quicksort :: [t] -> [t]
quicksort [ ] = [ ]
quicksort (a : x) = quicksort menores ++ [a] ++ quicksort maiores
where menores = [y | y <- x, y <= a]
maiores = [y | y <- x, y > a]
N
ao e fant
astica esta denicao? Sugiro ao leitor vericar a implementacao deste algoritmo
utilizando alguma linguagem imperativa como C, C++ ou Java e observando as diferencas em
facilidade de entendimento e de implementacao.
Mais exemplos.
1. A funcao fazpares:
fazpares :: [t] -> [u] -> [(t,u)]
fazpares l m = [ (a, b) | a <- l, b <- m]
fazpares [1,2,3] [4,5] = [(1,4), (1.5), (2,4), (2,5), (3,4), (3,5)]
2. A funcao pares:
pares :: Int -> [(Int, Int)]
pares n = [ (a, b) | a <- [1 .. n], b <- [1 .. a]]
pares 3 = [ (1,1), (2,1), (2,2), (3,1), (3,2), (3,3)]
3. Os tri
angulos retangulos:
trianguloretangulo n = [ (a, b, c) | a <- [2 .. n], b <- [a+1 .. n],
c <- [b+1 .. n], a * a + b * b == c * c]
trianguloretangulo 100 = [(3,4,5), (5,12,13), (6,8,10), ..., (65,72,97)]
Coment
arios
No exemplo 1) deve ser observada a forma como as expressoes sao construdas. O primeiro
elemento escolhido, a, vem da lista l. Para ele, s
ao construdos todos os pares possveis com
os elementos, b, que vem do outro gerador, a lista m. Assim toma-se o elemento 1 da lista
[1,2,3] e formam-se os pares com os elementos 4 e 5 da lista [4,5]. Agora escolhe-se o segundo
elemento da lista l, 2, e formam-se os pares com os elementos da lista m. Finalmente, repete-se
este processo para o terceiro elemento da lista l. Esta forma de construcao tem import
ancia
fundamental, sendo responsavel pela construcao de listas potencialmente innitas, um t
opico
descrito no pr
oximo Captulo. Neste exemplo, tambem se nota que uma expressao ZF pode n
ao
ter qualquer express
ao boolena.
No exemplo 2) deve-se notar a import
ancia que tem ordem em que os geradores sao colocados.
Se o gerador de b viesse antes do gerador de a, ocorreria um erro.
No exemplo 3) tambem deve ser observada a ordem em que os geradores foram colocados
para que seja possvel a geracao correta dos tri
angulos retangulos.
A funcao livrosEmprestados, denida no incio deste Captulo, pode ser re-denida da
seguinte forma:
livrosEmprestados :: BancodeDados -> Pessoa -> [Livro]
livrosEmprestados db fulano = [liv | (pes, liv) <- db, pes == fulano]
80
Exerccios:
1. Re-implemente as funcoes de atualizacao do Banco de Dados para a Biblioteca, feita no
incio do Captulo, usando compreens
ao de listas, em vez de recursao explcita.
2. Como pode a funcao membro :: [Int] > Int > Bool ser denida usando compreensao de listas e um teste de igualdade?
Exemplo. Vamos agora mostrar um exemplo mais completo, baseado na referencia [35], que
mostra algumas das possibilidades que as compreensoes oferecem. Seja um processador de texto
simples que organiza um texto, identando-o pela esquerda, como por exemplo:
Maria
gostava de bananas e
estava apaixonada
por
Joaquim e
tomou
veneno para
morrer.
Este pequeno trecho deve ser transformado em um texto mais organizado, cando da seguinte
forma:
Maria gostava de bananas e estava apaixonada
por Joaquim e tomou veneno para morrer.
Para isto, vamos construir algumas funcoes para realizar tarefas auxiliares. Inicialmente,
devemos observar que uma palavra e uma sequencia de caracteres que nao tem espacos em
branco dentro dela. Os espacos em branco sao denidos da seguinte forma:
espacoEmBranco :: [Char]
espacoEmBranco = [\n, \t, ]
Vamos denir a funcao pegaPalavra que, quando aplicada a uma string, retira a primeira
palavra desta string se a string n
ao iniciar com um espaco em branco. Assim pegaPalavra
bicho besta= bicho e pegaPalavra bicho= porque a string e iniciada com um
caractere em branco.
Uma denicao para ela pode ser feita, usando a funcao pertence que verica se um determinado caractere a pertence, ou n
ao, a uma string:
pertence :: Char -> [Char] -> Bool
pertence _ [ ] = False
pertence c (a:x)
|c == a
= True
|otherwise
= pertence c x
pegaPalavra :: String -> String
pegaPalavra [ ] = [ ]
pegaPalavra (a:x)
|pertence a espacoEmBranco
= [ ]
|otherwise
= a : pegaPalavra x
J
a a funcao tiraPalavra, quando aplicada a uma string, retira a primeira palavra da string
e retorna a string restante, tendo como seu primeiro caractere o espaco em branco. Assim,
tiraPalavra bicho feio= feio.
81
4.4
Fun
c
oes de alta ordem
let quad
quadrado x
sucessor x
compoe (f,g)
in quad 3
=
=
=
=
A resposta a esta aplicacao e 16. O interesse maior aqui esta na denicao da funcao compoe.
Ela toma um par de par
ametros, f e g (ambos funcoes) e retorna uma outra funcao, h, cujo
efeito de sua aplicacao e a composicao das funcoes f e g.
A partir destes exemplos, podemos caracterizar duas ferramentas importantes, a saber:
1. Um novo mecanismo para passar dois ou mais parametros para uma funcao. Apesar da
funcao soma ter sido declarada com apenas um par
ametro, poderamos chama-la, por
exemplo, como soma 3 4, que daria como resultado 7. Isto signica que soma 3 tem
como resultado uma outra funcao que, aplicada a 4, d
a como resultado o valor 7.
2. Um mecanismo de aplicacao parcial de funcoes. Isto quer dizer que, se uma funcao for
declarada com n par
ametros, podemos aplica-la a m destes parametros, mesmo que m
seja menor que n.
As funcoes de alta ordem sao usadas de forma intensa em programas funcionais, permitindo
que computacoes complexas sejam expressas de forma simples. Vejamos algumas destas funcoes,
muito utilizadas na pr
atica da programacao funcional.
4.4.1
A fun
c
ao map
Um padr
ao de computacao que e explorado como funcao de alta ordem envolve a criacao de uma
lista (listaNova) a partir de uma outra lista (listaVelha), onde cada elemento de listaNova
tem o seu valor determinado atraves da aplicacao de uma funcao a cada elemento de listaVelha.
Suponhamos que se deseja transformar uma lista de nomes em uma nova lista de tuplas, em
que cada nome da primeira lista e transformado em uma tupla, onde o primeiro elemento seja o
pr
oprio nome e o segundo seja a quantidade de caracteres do nome. Para isso, ser
a construda
uma funcao listaTupla de forma que
listaTupla ["Dunga","Constantino"] = [("Dunga",5),("Constantino",11)]
Antes vamos criar a funcao auxiliar tuplaNum que, aplicada a um nome, retorna a tupla
formada pelo nome e a quantidade de caracteres do nome.
tuplaNum :: [Char] -> ([Char], Int)
tuplaNum n = (n, length n)
Agora a funcao listaTupla pode ser denida da seguinte maneira:
listaTupla :: [String] -> [(String, Int)]
listaTupla [ ]
=
[ ]
listaTupla (a : x) = (tuplaNum a) : listaTupla x
Vamos agora, supor que se deseja transformar uma lista de inteiros em uma outra lista de
inteiros, onde cada valor inteiro da primeira lista seja transformado em seu dobro, ou seja,
dobraLista [3, 2, 5] = [6, 4, 10]
85
Como na denicao da funcao anterior, vamos construir a funcao auxiliar, dobra, da seguinte
forma:
dobra :: Int -> Int
dobra x = 2*x
dobraLista :: [Int] -> [Int]
dobraLista [ ]
= [ ]
dobraLista (a : x) = (dobra a) : dobraLista x
Analisando as denicoes listaTupla e dobraLista, observamos a presenca de um padr
ao
que e o de aplicar uma funcao a cada elemento da lista, ou seja, as duas funcoes percorrem as
listas aplicando uma funcao a cada um de seus elementos.
Uma outra opcao e construir uma funcao de alta ordem, destinada a realizar esta varredura, aplicando uma funcao a cada elemento da lista. A funcao a ser aplicada e passada como
par
ametro para a funcao de alta ordem. Neste caso, a funcao de alta ordem e chamada de mapeamento, simbolizado pela funcao pre-denida map, denida em Haskell da seguinte forma:
map :: (t -> u) -> [t] -> [u]
map f [ ]
= [ ]
map f (a : x) = (f a) : (map f x)
Assim, as denicoes anteriores de listaTupla e dobraLista podem ser re-denidas da seguinte forma:
listaTupla :: [String] -> [(String, Int)]
listaTupla x = map tuplaNum x
dobraLista :: [Int] -> [Int]
dobraLista x = map dobra x
Vejamos mais alguns exemplos:
duplica, triplica :: Int -> Int
duplica n = 2 * n
triplica n = 3 * n
duplicaLista, triplicaLista :: [Int] -> [Int]
duplicaLista l = map duplica l
triplicaLista l = map triplica l
map duplica [4,5]
=
=
=
=
=
=
=
mais f
E
acil entender a denicao, porque torna claro que se trata de um mapeamento, por
causa da funcao map. Precisa apenas entender a funcao mapeada.
mais f
E
acil modicar as denicoes das funcoes a serem aplicadas, se isto for necessario.
mais f
E
acil reutilizar as denicoes.
Exemplo: as funcoes de analise de vendas, mostradas no Captulo anterior, foram denidas
para analisar uma funcao xa: vendas. Agora, a funcao totalVendas pode ser dada por
totalVendas n = map vendas n
Muitas outras funcoes podem ser denidas usando map. Por exemplo,
somaQuad :: Int -> Int
somaQuad n = map quad n
quad :: Int -> Int
quad x = x * x
Exerccios
1. De denicoes de funcoes que tome uma lista de inteiros l e
retorne a lista dos quadrados dos elementos de l,
retorne a soma dos quadrados dos elementos de l e
verique se todos os elementos da lista sao positivos.
2. Escreva denicoes de funcoes que
de o valor mnimo de uma funcao aplicada a uma lista de 0 a n,
teste se os valores de f sobre as entradas 0 a n sao todas iguais.
teste se todos os valores de f aplicada `as entradas de 0 a n sao maiores ou iguais a
zero e
teste se os valores f 0, f 1 ate f n estao em ordem crescente.
3. Estabeleca o tipo e dena uma funcao trwice que toma uma funcao de inteiros para
inteiros e um inteiro e retorna a funcao aplicada `a entrada tres vezes. Por exemplo, com
a funcao triplica e o inteiro 4 como entradas, o resultado e 108.
4. De o tipo e dena uma funcao iter de forma que iter n f x = f ( f ( f . . . (f x) . .
.)), onde f ocorre n vezes no lado direito da equacao.
Por exemplo, devemos ter: iter 3 f x = f ( f ( f x )) e iter 0 f x = x.
5. Usando iter e duplica, dena uma funcao que aplicada a n retorne 2n .
87
4.4.2
Fun
c
oes an
onimas
J
a foi visto, e de forma ent
atica, que, nas linguagens funcionais, as funcoes podem ser usadas
como parametros para outras funcoes. No entanto, seria um desperdcio denir uma funcao que
so pudesse ser utilizada como par
ametro para outra funcao. Isto implicaria que a funcao so
fosse utilizada neste caso e nem um outro mais. No caso da secao anterior, as funcoes dobra
e tuplaNum, possivelmente, so sejam utilizadas como argumentos das funcoes dobraLista e
listaTupla.
Uma forma de declarar funcoes para serem utilizadas apenas localmente e usar a clausula
where. Por exemplo, dado um inteiro n, vamos denir uma funcao que retorne uma outra
funcao de inteiro para inteiro que adiciona n a seu argumento.
somaNum :: Int -> (Int -> Int)
somaNum n = h where h m = n + m
Quando se necessita especicar um valor, normalmente, se declara um identicador para isto.
Esta tambem tem sido a forma utilizada com as funcoes, ou seja, declara-se uma funcao com
um nome e, quando necessaria, e referenciada atraves de seu nome. Uma alternativa, possvel
em Haskell, consiste em declarar uma funcao apenas no ponto de chamada. Estas funcoes sao
as func
oes an
onimas. As funcoes anonimas se baseiam na notacao do -calculo, visto no
Captulo 2. Esta forma e mais compacta e mais eciente. A funcao somaNum, denida acima
usando a cl
ausula where, pode ser escrita, de forma anonima, da seguinte forma:
\m -> n + m
As funcoes anonimas permitem um acesso direto a funcoes, sem identicadores. Nese caso,
as funcoes dobraLista e listaTupla podem ser denidas da seguinte forma:
dobraLista x = map (\n -> 2*n) x
listaTupla x = map (\n -> (n, length n)) x
Vamos agora analisar como a sintaxe de uma funcao an
onima e feita. Uma denicao an
onima
e dividida em duas partes: uma antes da exa e a outra depois dela. Estas duas partes tem as
seguintes interpretacoes:
antes da exa vem os argumentos (neste caso, apenas n) e
depois da exa vem o resultado.
A barra invertida no incio (\) indica que se trata de uma funcao an
onima. A \ e o caractere
mais parecido com a letra grega , usada no -calculo.
Vejamos a funcao comp2, mostrada gracamente na Figura 4.1. Na realidade, trata-se de
uma funcao g que recebe como entrada dois argumentos, no caso f x e f y, que s
ao os resultados
das aplicacoes da funcao f aos argumentos x e y, ou seja g (f x) (f y).
A denicao de comp2 e
comp2 :: (a -> b) -> (b -> b -> c) -> (a -> a -> -c)
comp2 f g = (\x y -> g (f x) (f y))
Para se adicionar os quadrados de 5 e 6, podemos escrever
comp2 quad soma 5 6
88
comp2 f g
x
f
g (f x) (f y)
onde quad e soma tem signicados obvios. De forma geral, sendo f denida por
f x y z = resultado
entao f pode ser denida anonimamente por
\x y z -> resultado
As fun
c
oes fold e foldr
Uma outra funcao de alta ordem, tambem de grande utilizacao em aplicacoes funcionais, e a
funcao fold, usada na combinacao de tens (folding). Ela toma como argumentos uma funcao
de dois argumentos e a aplica aos elementos de uma lista. O resultado e um elemento do tipo
dos elementos da lista. Vamos ver sua denicao formal e exemplos de sua aplicacao.
fold :: (t -> t -> t) -> [t] -> t
fold f [a] = a
fold f (a:b:x) = f a (fold f (b:x))
Exemplos:
fold (||) [False, True, False]
=
=
=
=
=
=
=
No entanto, existe um pequeno problema na denicao de fold. Se ela for aplicada a uma
funcao e uma lista vazia ocorrera um erro. Para resolver este impasse, foi pre-denida, em
Haskell, uma outra funcao para substituir fold, onde este tipo de erro seja resolvido. Esta e a
funcao foldr, denida da seguinte forma:
foldr :: (t -> u -> u) -> u -> [t] -> u
foldr f s [ ] = s
foldr f s (a : x) = f a (foldr f s x)
Vamos vericar como algumas funcoes sao denidas usando foldr.
Exemplos:
89
A fun
c
ao filter
A funcao filter e uma outra funcao de alta ordem, pre-denida em todas as linguagens funcionais,
de bastante utilizacao. Resumidamente, ela escolhe dentre os elementos de uma lista, aqueles
que tem uma determinada propriedade. Vejamos alguns exemplos:
1. filter ehPar [2,3,4] = [2,4].
2. Um n
umero natural e perfeito se a soma de seus divisores, incluindo o n
umero 1, for o
pr
oprio n
umero. Por exemplo, 6 e o primeiro n
umero natural perfeito, porque 6 = 1+2+3.
Vamos denir uma funcao que mostra os n
umeros perfeitos entre 0 e m.
divide :: Int -> Int -> Bool
divide n a = n mod a == 0
fatores :: Int -> [Int]
fatores n = filter (divide n) [1..(n div 2)]
perfeito :: Int -> Bool
perfeito n = sum (fatores n) == n
perfeitos m = filter perfeito [0..m]
E se quizermos os primeiros m n
umeros perfeitos?
3. A lista de todos os n
umeros pares maiores que 113 e menores ou iguais a 1000, que sejam
perfeitos: filter perfeito [y | y < [114 .. 1000], ehPar y].
4. Selecionando elementos: filter digits 18 Marco 1958 = 181958
A denicao formal de filter e:
filter :: (t -> Bool) -> [t] -> [t]
filter p [ ] = [ ]
filter p (a : x)
ou
|p a
= a : filter p x
|otherwise = filter p x
4.5
filter p x = [a | a <- x, p a]
Polimorfismo
Uma caracterstica muito importante das linguagens funcionais e que suas denicoes podem
ser polimorcas, um mecanismo que aumenta o poder de expressividade de qualquer linguagem. Polimorsmo e uma das caractersticas responsaveis pela alta produtividade de software,
proporcionada pelo aumento da reusabilidade.
90
4.5.1
Tipos vari
aveis
4.5.2
Alguma diculdade pode surgir nas denicoes dos tipos das funcoes quando elas envolvem tipos
vari
aveis, uma vez que podem existir muitas instancias de um tipo vari
avel. Para resolver este
dilema, e necessario que o tipo da funcao seja o tipo mais geral possvel.
Um tipo w de uma funcao f e o tipo mais geral de f se todos os tipos de f forem inst
ancias
de w.
O tipo [t] > [t] > [(t, t)] e uma inst
ancia do tipo da funcao zip, mas nao e o tipo
mais geral, porque [Int] > [Bool] > [(Int, Bool)] e um tipo para zip, mas nao e uma
instancia de [t] > [t] > [(t, t)].
Exemplos de algumas fun
c
oes polim
orficas.
91
4.6
Induc
ao estrutural
J
a vimos formas de se provar propriedades em Haskell. No entanto, quando estas propriedades
envolvem listas, existe uma forma especca de serem provadas, que e a inducao estrutural. Podese dizer que inducao estrutural e o metodo de inducao matematica aplicado `as listas nitas. As
listas innitas n
ao sao tratadas, uma vez que elas nao sao estruturas model
aveis na Computacao.
Os computadores sao maquinas com memorias limitadas, apesar de poderem ser grandes, mas
sao nitas. Apesar de alguns autores se referirem a`s listas innitas, o que realmente eles se
referem sao as listas potencialmente innitas, que s
ao implementadas em Haskell atraves de
um mecanismo de avaliacao lazy, um t
opico a ser visto mais adiante. Deve ser lembrado que a
lista vazia, [ ], e uma lista nita e a lista nao vazia, (a:x), e uma lista nita, se a lista x for
nita.
Esquema de prova
O esquema de provas mostrado a seguir e devido a Simon Thompson [35]. Apesar de muito
formal, ele deve ser seguido, principalmente, por iniciantes, que ainda n
ao tem experiencia com
provas de programas. Para estes usu
arios, e recomendavel utiliza-lo como forma de treinamento.
92
Muitas pessoas tendem a querer chegar `a conclusao de uma prova forcando situacoes, sem a
argumentacao adequada e este tipo de vcio h
a que ser evitado, a qualquer custo. A falta de
domnio nesta area pode levar o usu
ario a conclusoes equivocadas.
Outro erro, comumente cometido por algumas pessoas n
ao afeitas a provas matematicas,
consiste em realizar provas sem se importar com as conclusoes das mesmas. A conclus
ao e parte
ntegrante do esquema de provas e, portanto, indispens
avel. Ela e o objetivo da prova. Sem
ela n
ao existe razao para todo um esforco a ser despendido nas fases anteriores. A conclus
ao
representa o desfecho de uma prova e representa a formalizacao de uma proposicao que passa a
ser verdadeira e pode ser utilizada em qualquer etapa de uma computacao.
O esquema de provas deve se constituir nos seguintes estagios:
Est
agio 0:
Est
agio 1:
Est
agio 2:
Est
agio 3:
Est
agio 4:
Vejamos agora, dois exemplos completos de esquemas de provas que envolvem todos os
estagios enumerados anteriormente.
Exemplo 1. Dadas as denicoes a seguir:
somaLista [ ] = 0
somaLista (a : x) = a + somaLista x
(1)
(2)
dobra [ ] = [ ]
dobra (a : x) = (2 * a) : dobra x
(3)
(4)
Provar que
Est
agio 0: o dobro da soma dos elementos de uma lista e igual a` soma dos elementos da
lista formada pelos dobros dos elementos da lista anterior.
Est
agio 1: somaLista (dobra x) = 2 * somaLista x
(5)
Est
agio 2:
sumList (dobra [ ]) = 2 * somaLista [ ] (6)
somaLista (dobra (a : x)) = 2 * somaLista (a : x) (7)
assumindo que somaLista (dobra x) = 2 * somaLista x (8)
Est
agio 3: Caso base:
lado esquerdo do caso base:
somaLista (dobra [ ])
= somaLista [ ]
=0
por (3)
por (1)
por (1)
pela aritmetica.
por (4)
por (2)
pela hip
otese de inducao
pela distributividade de *.
por (2).
(2)
x ++ (y ++ z) = (x ++ y) ++ z
Est
agio 2:
caso base: [ ] ++ (y ++ z) = ([ ] ++ y) ++ z (3)
passo indutivo: (a : x) ++ (y ++ z) = ((a : x) ++ y) ++ z (4) assumindo
que x ++ (y ++ z) = (x ++ y) ++ z (5)
Est
agio 3:
caso base: lado esquerdo
[ ] ++ (y ++ z)
= y ++ z
por (1)
por (1)
por (2)
por (2)
por (2)
pela hip
otese de inducao.
Conclus
ao: como a assertiva e valida para o caso base e para o passo indutivo, ent
ao ela e
verdadeira para todas as listas nitas.
Exerccios
1. Prove que, para todas as listas nitas x, x ++ [ ] = x.
2. Tente provar que x ++ (y ++ z) = (x ++ y) ++ z usando inducao estrutural sobre
z. H
a alguma coisa esquisita com esta prova? O que?
3. Prove que, para todas as listas nitas x e y, sumList (x ++ y) = sumList x +
sumList y e que sumList (x ++ y) = sumList (y ++ x).
4. Mostre que, para todas as listas nitas x e y,
double (x ++ y) = double x ++ double y e
length (x ++ y) = length x + length y.
5. Prove por inducao sobre x que, para todas as listas nitas x,
sumList (x ++ (a : y)) = a + sumList (x ++ y).
6. Prove, usando ou n
ao o exerccio anterior, que para todas as listas nitas x,
sumList (double x) = sumList (x ++ x).
4.7
Composi
c
ao de fun
c
oes
Uma forma simples de estruturar um programa e constru-lo em etapas, uma apos outra, onde
cada uma delas pode ser denida separadamente. Em programacao funcional, isto e feito atraves
da composicao de funcoes, uma propriedade matematica so implementada nestas linguagens. A
sua sintaxe obedece aos princpios b
asicos empregados na matematica e aumenta, enormemente,
a expressividade do programador. A funcao preenche foi denida anteriormente da seguinte
forma:
preenche :: String -> [Linha]
preenche st = divideLinhas (divideEmPalavras st)
divideEmPalavras :: String -> [Palavra]
divideLinhas :: [Palavra] -> [Linha]
preenche pode ser re-escrita como: preenche = divideLinhas . divideEmPalavras
Esta forma de codicacao torna a composicao explcita sem a necessidade de aplicar cada
lado da igualdade a um argumento. Da Matem
atica herdamos a notacao de ., onde
(f . g) x = f ( g x )
Como e sabido da Matem
atica, nem todo par de funcoes pode ser composto. O tipo da sada
da funcao g tem de ser o mesmo tipo da entrada da funcao f. O tipo de . e: ( . ) :: (u >
v) > (t > u) > (t > v).
A composicao e associativa, ou seja, f . (g . h) = (f . g) . h, que deve ser interpretado
como faca h, depois faca g e nalmente faca f .
95
4.7.1
Composi
c
ao avan
cada
4.7.2
=
=
=
=
=
Nesta denicao, usamos o fato de que f . id = f. Estamos tratando de uma nova especie de
igualdade, que e a igualdade de duas funcoes. Mas como isto pode ser feito? Para isto, devemos
96
Isto signica que para um argumento x, qualquer, as duas funcoes se comportam exatamente
da mesma forma.
Vamos fazer uma pequena discussao sobre o que signica a igualdade entre duas funcoes.
Existem dois princpios que devem ser observados quando nos referimos a` igualdade de funcoes.
S
ao eles:
Princpio da extensionalidade: Duas funcoes, f e g, sao iguais se elas produzirem
exatamente os mesmos resultados para os mesmos argumentos.
Princpio da intencionalidade: Duas funcoes, f e g sao iguais se tiverem as mesmas
denicoes.
Se estivermos interessados nos resultados de nossos programas, tudo o que nos interessa
sao os valores dados pelas funcoes e nao como estes valores sao encontrados. Em Haskell,
devemos usar extensionalidade quando estivermos interessados no comportamento das funcoes.
Se estivermos interessados na eciencia ou outros aspectos de desempenho de programas devemos
usar a intencionalidade.
Exerccios
1. Mostre que a composicao de funcoes e associativa, ou seja, f, g e h, f . (g . h) = (f .
g) . h
2. Prove que n Z + , iter n id = id.
3. Duas funcoes f e g sao inversas se f . g = id e g . f = id. Prove que as funcoes curry
e uncurry, denidas a seguir, s
ao inversas.
curry :: ((t, u) -> v) -> (t -> u -> v)
curry f (a, b) = f a b
uncurry :: (t -> u -> v) -> ((t, u) -> v)
uncurry g a b = g (a, b)
Exemplo: Seja a denicao de map e da composicao de funcoes dadas a seguir. Mostre que
map (f. g) x = (map f . map g) x
map f [ ] = [ ]
map f (a : x) = f a : map f x
(f . g) x = f (g x)
(1)
(2)
(3)
Prova:
Caso base: a lista vazia, [ ]:
Lado esquerdo
map (f . g) [ ] = [ ]
por (1)
Lado direito
(map f . map g) [ ]
= map f (map g [ ])
=map f [ ]
=[ ]
97
por (3)
por (1)
por (1)
Passo indutivo: (a : x)
Lado esquerdo
map (f . g) (a : x)
=(f . g) a : map (f . g) x
=f (g a) : map (f . g) x
=f (g a) : (map f . map g) x
(2)
(3)
(hi)
Lado direito
(map f . map g)(a:x)
= map f (map g (a:x))
=map f (g a) : map f (map g x)
=f (g a) : (map f . map g) x
(3)
(2)
(3)
Conclus
ao. Como a propriedade e valida para a lista vazia e para a lista nao vazia, entao ela
e valida para qualquer lista homogenea nita.
Exerccio: Prove que para todas as listas nitas l e funcoes f, concat (map (map f ) l) =
map f (concat l).
4.8
Aplica
c
ao parcial
Uma caracterstica importante de Haskell e que proporciona uma forma elegante e poderosa de
construcao de funcoes e a avaliacao parcial que consiste na aplicacao de uma funcao a menos
argumentos que ela realmente precisa. Por exemplo, seja a funcao multiplica que retorna o
produto de seus argumentos:
multiplica :: Int -> Int -> Int
multiplica a b = a * b
Esta funcao foi declarada para ser usada com dois argumentos. No entanto, ela pode ser
chamada como multiplica 2. Esta aplicacao retorna uma outra funcao que, aplicada a um
argumento b, retorna o valor 2*b. Esta caracterstica e o resultado do seguinte princpio em
Haskell: uma func
ao com n argumentos pode ser aplicada a r argumentos, onde r n. Como
exemplo, a funcao dobraLista pode ser denida da seguinte forma:
dobraLista :: [Int] -> [Int]
dobraLista = map (multiplica 2)
onde multiplica 2 e uma funcao de inteiro para inteiro, a aplicacao de multiplica a um de
seus argumentos, 2, em vez de ser aplicada aos dois. map (multiplica 2) e uma funcao do
tipo [Int] > [Int], dada pela aplicacao parcial de map.
Como e determinado o tipo de uma aplicacao parcial? Pela regra do cancelamento: se uma
funcao f tem o tipo t1 > t2 > ... > tn > t e e aplicada aos argumentos e1 :: t1 , e2:: t2 ,
ao o tipo do resultado e dado pelo cancelamento dos tipos t1 at
e
... , ek :: tk , com k n, ent
tk , dando o tipo tk+1 > tk+2 > ... tn t.
Por exemplo,
multiplica
multiplica
dobraLista
dobraLista
Mas anal, quantos argumentos tem realmente uma funcao em Haskell? Pelo exposto, a
resposta correta a esta questao e 1 (UM).
Isto signica que uma funcao do tipo Int > Int > Int e do mesmo tipo que Int
> (Int > Int). Neste u
ltimo caso, esta explcito que esta funcao pode ser aplicada a um
argumento inteiro e o seu resultado e uma outra funcao que recebe um inteiro e retorna outro
inteiro. Exemplicando,
98
multiplica
:: Int -> Int -> Int
multiplica 4 :: Int -> Int
multiply 4 5 :: Int
ou
Associatividade:
A aplicacao de funcao e associativa `a esquerda, ou seja:
f a b = (f a) b
enquanto a exa e associativa pela direita:
t > u > v = t > (u > v).
4.8.1
Se
c
ao de operadores
Uma decorrencia direta das aplicacoes parciais que representa uma ferramenta poderosa e elegante em algumas linguagens funcionais e, em particular, em Haskell, s
ao as secoes de operadores.
As secoes sao operacoes parciais, normalmente relacionadas com as operacoes aritmeticas. Nas
aplicacoes, elas sao colocadas entre parenteses. Por exemplo,
(+2) e a funcao que adiciona algum argumento a 2,
(2+) a funcao que adiciona 2 a algum argumento,
(>2) a funcao que retorna True se um inteiro for maior que 2,
(3:) a funcao que coloca o inteiro 3 na cabeca de uma lista,
(++\n) a funcao que coloca o caractere \n ao nal de uma string.
Uma secao de um operador op coloca o argumento no lado que completa a aplicacao. Por
exemplo, (op a) b = b op a e (a op) b = a op b.
Exemplos:
map (+1) > . > filter ( >0).
dobra = map (*2).
pegaPares = filter (( ==0) . (mod2)).
A funcao inquilinos, denida no incio deste Captulo, normalmente, e escrita da seguinte
forma:
inquilinos db pes = map snd (filter ehPes db)
where
ehPes (p, b) = (p == pes).
No entanto, ela pode ser escrita em forma de secao da seguinte maneira, o que a torna
muito mais elegante.
inquilinos db pes = map snd (filter ((==pes) . fst) db).
4.8.2
Currificac
ao
Este nome tambem foi cunhado em homenagem a Haskell Brooks Curry, por sua pesquisa na
l
ogica e no -calculo. Na realidade, Sch
onnkel foi o pioneiro, mas Haskell foi quem mais
99
utilizou esta propriedade em suas pesquisas: nas linguagens funcionais, uma func
ao de aridade
n e equivalente a n func
oes de aridade 1.
Exemplo.
Este e mais um exemplo que mostra o poder de expressividade de Haskell. Trata-se da criacao
de um ndice remissivo, encontrado na maioria dos livros tecnicos. Este exemplo e baseado no
livro de Simon Thompson [35]. Algumas funcoes ja foram denidas anteriormente, no entanto
elas serao novamente denidas aqui para evitar ambig
uidades. Vamos mostrar, inicialmente, os
tipos de dados utilizados na simulacao.
type Doc = String
type Linha = String
type Palavra = String
fazIndice :: Doc -> [ ([Int], Palavra) ]
Neste caso, o texto e uma string como a seguinte:
doc :: Doc
doc = "Imagine theres no heaven\nIts easy if you try\nNo hell
below us\nAbove us only sky\nImagine all the people\nliving
for today"
Vamos utilizar este trecho e mostrar como ele vai ser transformado com a aplicacao das
funcoes que serao denidas para realizar operacoes sobre ele:
Dividir doc em linhas, cando assim:
[Imagine theres no heaven,
Its easy if you try,
No hell below us,
Above us only sky,
Imagine all the people,
living for today]
Esta operacao e realizada por divTudo :: Doc > [Linha]
Agora devemos emparelhar cada linha com seu n
umero de linha:
[(1, Imagine theres no heaven),
(2, Its easy if you try),
(3, No hell below us),
(4, Above us only sky),
(5, Imagine all the people),
(6, Living for today)]
Esta operacao e realizada por numLinhas :: [Linha] > [(Int, Linha)]
Temos agora que dividir as linhas em palavras, associando cada palavra com o n
umero da
linha onde ela ocorre
[(1, Imagine), (1, theres), (1, no), (1, heaven),
(2, Its), (2, easy), (2, if), (2, you), (2, try),
(3, No), (3, hell), (3, below), (3, us),
(4, Above), (4, us), (4, only), (4, sky),
(5, Imagine), (5, all), (5, the), (5, people),
(6, Living), (6, for), (6, today)]
Esta operacao e realizada por todosNumPal :: [(Int, Linha)] > [(Int, Palavra)]
100
Agora e necessario ordenar esta lista em ordem alfabetica das palavras e, se a mesma
palavra ocorre em mais de uma linha, ordenar por linha.
[(4, Above), (5, all), (3, below), (2, easy), (6, for), (1, heaven), (3, hell),
(2, Its), (5, Imagine), (2, if), (6, Living), (3, No), (1, no),
(4, only), (5, people), (4, sky), (5, the), (1, theres), (6, today),
(2, try), (3, us), (4, us), (2, you)]
Esta operacao e realizada por ordenaLista :: [(Int, Palavra)] > [(Int, Palavra)]
Agora temos que modicar a lista de forma que cada palavra seja emparelhada com a lista
unit
aria das linhas onde ela ocorre:
[([4], Above), ([5], all), ([3], below), ([2], easy), ([6], for), ([1], heaven),
([3], hell), ([2], Its), ([5], Imagine), ([2], if), ([6], Living), ([3], No),
([1], no), ([4], only), ([5], people), ([4], sky), ([5], the), ([1], theres),
([6], today), ([2], try), ([3], us), ([4], us), ([2], you)]
Esta operacao e realizada por fazListas :: [(Int, Palavra)] > [([Int], Palavra)]
Agora devemos juntar as linhas que contem uma mesma palavra em uma mesma lista de
linhas
[([4], Above), ([5], all), ([3], below), ([2], easy), ([6], for), ([1], heaven),
([3], hell), ([2], Its), ([5], Imagine), ([2], if), ([6], Living), ([3], No),
([1], no), ([4], only), ([5], people), ([4], sky), ([5], the), ([1], theres),
([6], today), ([2], try), ([3,4], us), ([2], you)]
Esta operacao e realizada por mistura :: [([Int], Palavra)] > [([Int], Palavra)]
Vamos agora diminuir a lista, removendo todas as entradas para palavras com menos de
4 letras
[([4], Above), ([3], below), ([2], easy), ([1], heaven), ([3], hell),
([2], Its), ([5], Imagine), ([6], Living),
([4], only), ([5], people), ([1], theres), ([6], today)]
Esta operacao e realizada por diminui :: [([Int], Palavra)] > [([Int], Palavra)]
Usando composicao avancada, para car mais claro o exemplo, temos:
fazIndice = divideTudo >.> -- Doc
-> [Linha]
numDeLinhas >.> -- [Linha]
-> [(Int, Linha)]
todosNumPal >.> -- [(Int, Linha)]
-> [(Int, Palavra)]
ordenaLista >.> -- [(Int, Palavra)]
-> [(Int, Palavra)]
fazListas
>.> -- [(Int, Palavra)]
-> [([Int], Palavra)]
mistura
>.> -- [([Int], Palavra)] -> [([Int], Palavra)]
diminui
-- [([Int], Palavra)] -> [([Int], Palavra)]
=======================================================================
divideTudo :: Doc -> [Linha]
divideTudo = tiraEspaco >.> pegaLinhas
tiraEspaco :: Doc -> Doc
tiraEspaco [ ] = [ ]
tiraEspaco (a : x)
|a == = tiraEspaco x
|otherwise = (a : x)
pegaLinha :: Doc -> [Linha]
pegaLinha [ ] = [ ]
101
pegaLinha (a : x)
|a /= \n = a : pegaLinha x
|otherwise = pegaLinha x
=======================================================================
numDeLinhas :: [Linha] -> [(Int, Linha)]
numDeLinhas lin = zip [1 .. length lin] lin
=======================================================================
Vamos considerar, inicialmente, apenas uma linha:
numDePalavras :: (Int, Linha) -> [(Int, Palavra)]
numDePalavras (num, linha) = map poeNumLinha (divideEmPalavras linha)
where poeNumLinha pal = (num, pal)
divideEmPalavra :: String -> [Palavra]
divideEmPalavra st = divide (tiraEspaco st)
divide :: String -> [Palavra]
divide [ ] = [ ]
divide st = (pegaPalavra st) : divide (tiraEspaco (tiraPalavra st))
tiraPalavra :: String -> String
tiraPalavra [ ] = [ ]
tiraPalavra (a : x)
|elem a espacoEmBranco = (a : x)
|otherwise = tiraPalavra x
espacoEmBranco = [\n, \t, ]
todosNumPal :: [(Int, Linha)] -> [(Int, Palavra)]
todosNumPal = concat . map numDePalavras
=======================================================================
Agora vamos denir a funcao de ordenacao usando quicksort:
compara :: (Int, Palavra) -> (Int, Palavra) -> Bool
compara (n1, w1) (n2, w2) = w1 < w2 || (w1 == w2 && n1 $<$ n2)
ordenaLista :: [(Int, Palavra)] -> [(Int, Palavra)]
ordenaLista [ ] = [ ]
ordenaLista (a : x) = ordenaLista menores ++ [a] ++ ordenaLista maiores
where menores = [b | b <- x, compara b a]
maiores = [b | b <- x, compara a b]
=======================================================================
fazListas :: [(Int, Palavra)] -> [([Int], Palavra)]
fazListas = map mklist where mklist (n, st) = ([n], st)
=======================================================================
mistura :: [([Int], Palavra)] -> [([Int], Palavra)]
mistura [ ] = [ ]
mistura [a] = [a]
mistura ((l1, w1) : (l2, w2) : rest)
102
4.9
Ate agora mostramos ao leitor formas de denir funcoes em Haskell sem nos preocupar com o
desempenho de cada implementacao. Nosso objetivo tem sido apenas construir denicoes, de
forma que elas funcionem. No entanto, este foi um objetivo inicial, uma vez que a preocupacao
com desempenho deve ser uma constante em qualquer programador. Nesta secao, vamos fazer
algumas observacoes relacionadas ao desempenho de funcoes, dando os primeiros passos nesta
direcao. Vamos iniciar com a analise do desempenho da funcao reverse, muito utilizada nas
denicoes de funcoes sobre listas.
4.9.1
O desempenho da fun
c
ao reverse
A funcao reverse inverte a ordem dos elementos de uma lista de qualquer tipo. Ela e pre-denida
em Haskell, e poderia ser facilmente denida por
reverse :: [t] -> [t]
reverse [ ] = [ ]
reverse (a : x) = reverse x ++ [a]
A despeito da simplicidade desta denicao, ela padece de um serio problema de desempenho.
Vamos vericar quantos passos seriam necessarios para inverter uma lista l de n elementos,
usando este denicao. A funcao chama a si mesma n vezes e, em cada uma destas chamadas,
chama ++, tail e head. As funcoes head e tail sao primitivas, ou seja, exigem tempo constante
para suas execucoes. A operacao ++ deve saltar para o nal de seu primeiro argumento para
concaten
a-lo com o segundo. O tempo total de reverse e dado pela soma
(n 1) + (n 2) + (n 3) + . . . + 1 + 0 =
n1
i=0
i=
n(n 1)
O(n2 )
2
pilha, ate que a pilha anterior que vazia. Neste ponto, a nova pilha ser
a a pilha anterior em
ordem inversa.
Vamos aplicar este mesmo raciocnio na implementacao da funcao reverse. Para isto usaremos duas listas, pl e sl, para simular as pilhas de livros. Vamos denir uma funcao auxiliar,
revaux que, aplicada `a duas listas, retira o elemento da cabeca da primeira, pl, e o coloca como
cabeca da segunda, sl. A funcao termina quando pl car vazia, e o resultado sera a lista sl.
Formalmente esta denicao e
revaux :: [t] -> [t] -> [t]
revaux pl sl
|pl == [ ]
= sl
|otherwise = revaux (tail pl) ((head pl) : sl)
reverse :: [t] -> [t]
reverse x = revaux x [ ]
Uma an
alise da complexidade desta denicao nos informa que ela e proporcional ao tamanho
da lista, ou seja, O(n) [24, 30, 7]. Esta e a forma como a funcao e implementada em Haskell.
4.9.2
O desempenho do quicsort
n2
2
5n
2
sendo n o tamanho da lista [30]. Isto signica que a complexidade de tempo para o pior caso e
O(n2 ). Esta complexidade e pior que a grande maioria dos algoritmos de ordenacao, apesar de
ser um caso pouco provavel de acontecer. Um caso mais realista consiste na hipotese de que a
lista original seja dividida em duas sub-listas de tamanhos pr
oximos. Neste caso, a analise da
eciencia de tempo medio se torna
TqsortAC (n) n log n + 2n 1
Neste hipotese, a complexidade do quicksort e O(n log n), bem melhor, que a complexidade
para o pior caso.
A an
alise da eciencia de espaco acumulado nos leva a resultados similares, ou seja
SqsortW C (n) =
n2
2
n
2
SqsortAC = n log n + n
Isto mostra que a complexidade de espaco do quicksort para o caso medio tambem e
O(n log n).
Para nalizar este Captulo, vamos colocar uma lista de exerccios sobre listas. Estes
exerccios devem ser resolvidos pelo leitor para adquirir domnio sobre a construcao de listas
para as diversas nalidades.
Exerccios.
1. Dena, em Haskell, uma funcao f que, dadas uma lista i de inteiros e uma lista l qualquer,
retorne uma nova lista constituda pela lista l seguida de seus elementos que tem posicao
104
***
*
***
*
***
O formato de cada n
umero e dinido por uma lista de inteiros que indicam quantos * se
repetem, seguidos de quantos brancos se repetem, ate o nal da matriz 5x3, comecando
da primeira linha ate a u
ltima:
nove, cinco, um, dois, tres, quatro, seis, sete oito, zero :: [Int]
nove
= [4,1,4,2,1,2,1]
cinco = [4,2,3,2,4]
um
= [0,2,1,2,1,2,1,2,1,2,1]
dois
= [3,2,5,2,3]
tres
= [3,2,4,2,4]
quatro = [1,1,2,1,4,2,1,2,1]
seis
= [4,2,4,1,4]
sete
= [3,2,1,2,1,2,1,2,1]
oito
= [4,1,5,1,4]
zero
= [4,1,2,1,2,1,4]
indicando que o n
umero nove e composto por 4 *s (tres na primeira linha e um na segunda),
seguida de 1 espaco, mais 4 *s, 2 espacos, 1 *, 2 espacos e 1 *. Construa funcoes para:
a. Dado o formato do n
umero (lista de inteiros) gerar a String correspondente de * e
espacos.
toString :: [Int] -> String
toString nove ==> **** ****
b. Faca uma funcao que transforma a String de *s e espacos em uma lista de Strings,
cada uma representando uma linha do LCD:
105
*,
*]
c. Faca uma funcao que pegue uma lista de Strings e a transforme em uma u
nica
String com
n entre cada uma delas:
showLinhas :: [Linha] -> String
showLinhas [***, * *, ***,
==> ***\n* *\n***\n *\n *
*,
*]
d. Faca uma funcao que pegue duas listas de linhas e transforme-as em uma u
nica lista
de linhas, onde as linhas originais se tornam uma u
nica, com um espaco entre elas:
juntaLinhas :: [LInha] ->
juntaLInhas [***, *
[***, *
==> [*** ***,
*
*, *
e. Faca uma funcao que, dado um inteiro, imprima-o usando *s, espacos e
ns. A funcao tem que funcionar para inteiros de 2 e 3 dgitos.
tolcd :: Int -> String
Dica: use as funcoes div e mod de inteiros, a funcao (!! e a lista numeros, dada a
seguir:
numeros :: [[Int]]
numeros
= [zero, um, dois, tres, quatro, cinco, seis, sete, oito, nove]
f. Faca uma funcao que, dada uma String de *s e espacos, retorne a representacao da
String como [Int] no formato usado no LCD, ou seja, a funcao inversa de toString.
toCompact :: String -> [Int]
toCompact **** ***
*
* = [4,1,4,2,1,2,1].
7. Dena, em Haskell, uma funcao que aplicada a uma lista l e a um inteiro n, retorne todas
as sublistas de l com comprimento maior ou igual a n.
4.10
Resumo
Este Captulo foi dedicado inteiramente ao estudo das listas em Haskell, completando o estudo
dos tipos primitivos adotados em Haskell, alem do tipo estruturado produto cartesiano, representado pelas tuplas. Ele se tornou necessario, dada a import
ancia que as listas em nas linguagens
funcionais. Foi dada enfase `as funcoes pre-denidas e a`s Compreensoes, tambem conhecidas
por express
oes ZF, mostrando a facilidade de se construir funcoes com esta ferramenta da linguagem. Vimos tambem a elegancia com que algumas funcoes, como por exemplo, o quicksort,
foram denidas. Finalmente foram vistas caractersticas importantes na construcao de funcoes
como: polimorsmo, composicao, avaliacao parcial e curricacao de funcoes.
Apesar dos tipos de dados estudados ate aqui j
a signicarem um avanco importante, Haskell
vai mais alem. A linguagem tambem permite a criacao de tipos abstratos de dados e tambem
dos tipos algebricos de dados, temas a serem estudados no proximo Captulo.
106
Grande parte deste estudo foi baseado nos livros de Simon Thompson [35] e de Richard Bird
[4]. Estas duas referencias representam o que de mais pr
atico existe relacionado com exerccios
usando listas. O livro de Paul Hudak [14] e tambem uma fonte de consulta importante, dada a
sua aplicacao a` multimdia.
Uma fonte importante de problemas que podem ser resolvidos utilizando as ferramentas aqui
mostradas e o livro de Steven S. Skiena e Miguel A. Revilla [33], que apresenta um cat
alogo dos
mais variados tipos de problemas, desde um nvel inicial ate problemas complexos e de solucao
complexa.
Outra fonte importante de problemas matem
aticos envolvendo grafos e o livro de Sriram
Pemmaraju e Steven Skiena [28]. Esta referencia implementa seus exerccios em uma ferramenta conhecida como Mathematica, e podem ser facilmente traduzidos para Haskell, dada
a proximidade sint
atica entre os programas codicados em Haskell e as solucoes adotadas na
Mathematica.
107
108
Captulo 5
5.1
Introdu
c
ao
Este Captulo e dedicado a` construcao de novos tipos de dados, mais complexos que os ate agora
apresentados, que s
ao os tipos algebricos e os tipos abstratos de dados.
Vamos iniciar este estudo com as classes de tipos, conhecidas mais comumente como type
class. O domnio deste tema e necessario porque as classes de tipos representam a forma usada
em Haskell para implementar sobrecarga de operadores. Alem disso, as classes de tipos tambem
sao utilizadas na implementacao dos tipos algebricos e dos tipos abstratos de dados.
Os tipos algebricos e os tipos abstratos de dados, apesar de mais complexos, representam
um aumento na expressividade e no poder de abstracao de Haskell. Por estes motivos, este
estudo deve ser de domnio pleno para quem deseja aproveitar as possibilidades que a linguagem
oferece. Neste Captulo, tambem e feito um estudo sobre o tratamento de erros, sobre as provas
de programas envolvendo tipos algebricos de dados e sobre os modulos em Haskell. O Captulo
termina com um estudo sobre os tipos abstratos de dados e sobre lazy evaluation, utilizada na
criacao de listas potencialmente innitas.
5.2
Classes de tipos
J
a foram vistas funcoes que atuam sobre valores de mais de um tipo. Por exemplo, a funcao
length pode ser empregada para determinar o tamanho de listas de qualquer tipo. Assim,
length e uma funcao polim
orca, ou seja, com apenas uma u
nica denicao, ela pode ser aplicada
a uma variedade de tipos. Por outro lado, algumas funcoes podem ser aplicadas a apenas alguns
tipos de dados, mas nao podem ser aplicadas a todos, e tem de apresentar varias denicoes, uma
para cada tipo. S
ao os casos das funcoes +, -, /, etc. Estas funcoes sao sobrecarregadas.
109
5.2.1
Fundamenta
c
ao das classes
A funcao elem, aplicada a um elemento e uma lista de valores do tipo deste mesmo elemento,
verica se este elemento pertence, ou nao, a` lista, retornando um valor booleano. Sua denicao
e a seguinte:
elem :: t -> [t] -> Bool
elem x [ ] = False
elem x (a : y) = x == a || elem x y
Analisando a denicao da funcao aplicada `a lista nao vazia, vericamos que e feito um teste
para vericar se o elemento x e igual a cabeca da lista (x==a). Para este teste e utilizada a
funcao de igualdade (==). Isto implica que a funcao elem so pode ser aplicada a tipos cujos
valores possam ser comparados pela funcao ==. Os tipos que tem esta propriedade formam
uma classe que, em Haskell, e denotada por Eq.
Em Haskell, existem dois operadores de igualdade: == e /=. Para valores booleanos, eles
sao denidos da seguinte forma:
(/=), (==) :: Bool -> Bool -> Bool
x == y = (x and y) or (not x and not y)
x /= y = not (x == y)
importante observar a diferenca entre == e =. O smbolo == e usado para denotar um
E
teste computacional para a igualdade, enquanto o smbolo = e usado nas denicoes e no sentido
matematico normal. Na Matem
atica, a assertiva double = square e uma armacao falsa e a
assertiva = e verdadeira, uma vez que qualquer coisa e igual a si pr
opria. No entanto, as
funcoes nao podem ser testadas quanto `a sua igualdade e o resultado da da avaliacao == e
tambem e n
ao True. Isto n
ao quer dizer que o avaliador seja uma m
aquina n
ao matematica
e sim, que seu comportamento e descrito por um conjunto limitado de regras matematicas,
escolhidas de forma que elas possam ser executadas mecanicamente [4].
O objetivo principal de se introduzir um teste de igualdade e ser capaz de usa-lo em uma
variedade de tipos distintos, n
ao apenas no tipo Bool. Em outras palavras, == e /= sao
operadores sobrecarregados. Estas operacoes serao denidas de forma diferente para cada tipo
e a forma adequada de introduz-las e declarar uma classe de todos os tipos para os quais ==
e /= v
ao ser denidas. Esta classe e pre-denida em Haskell e e denominada Eq.
110
A forma de declarar Eq como a classe dos tipos que tem os operadores ==e /= e a seguinte:
class Eq t where
(==), (/=) :: t -> t -> Bool
Esta declaracao estabelece que a classe de tipos Eq contem duas funcoes membros, ou
metodos, == e /=. Estas funcoes tem o seguinte tipo:
(==), (/=) :: Eq t => t -> t -> Bool
Agora o leitor pode entender o resultado mostrado pelo sistema Haskell quando detecta
algum erro de tipo na declaracao de funcoes. Normalmente, o sistema se refere a alguma classe
de tipos nestes casos.
5.2.2
Fun
c
oes que usam igualdade
A funcao
todosIguais :: Int -> Int -> Int -> Bool
todosIguais m n p = (m == n) && (n == p)
verica se tres valores inteiros sao iguais, ou nao. No entanto, em sua denicao, n
ao e feita
qualquer restricao que a obrigue a ser denida somente para valores inteiros. A u
nica exigencia
feita aos elementos m, n e p e que eles possam ser comparados atraves da funcao de igualdade
==. Dessa forma, seu tipo pode ser um tipo t, desde que seus elementos possem ser comparados
pela funcao ==. Isto d
a a` funcao todosIguais um tipo mais geral da seguinte forma:
todosIguais :: Eq t => t -> t -> t -> Bool
signicando que ela n
ao e utilizada apenas sobre os tipos inteiros, mas sim, a tipos que estejam
na classe Eq, ou seja, para os quais seja denida uma funcao de igualdade (==). A parte antes
do sinal => e chamada de contexto. A leitura deste novo tipo deve ser: se o tipo t esta na classe
Eq, ou seja, se a funcao de igualdade, == estiver denida para este tipo t, ent
ao todosIguais
tem o tipo t > t > t > Bool. Isto signica que a funcao todosIguais pode ter os
seguintes tipos, entre outros: Int > Int > Int > Bool ou Char > Char > Char
> Bool ou ainda (Int, Bool) > (Int, Bool) > (Int, Bool) > Bool, uma vez que
todos estes tipos tem uma funcao == denida para eles.
Vejamos agora o que acontece ao tentarmos aplicar a funcao todosIguais a argumentos do
tipo funcao, como por exemplo,
suc :: Int -> Int
suc = (+1)
da seguinte forma: todosIguais suc suc suc.
O resultado mostrado pelos compiladores Haskell ou Hugs e ERROR: Int > Int is not
an instance of class Eq, signicando que n
ao existe uma denicao da funcao == para o tipo
Int > Int (o tipo de suc).
111
5.2.3
Assinaturas e inst
ancias
J
a foi visto que a operacao de igualdade (==) e sobrecarregada, o que permite que ela seja
utilizada em uma variedade de tipos para os quais esteja denida, ou seja, para as inst
ancias
da classe Eq. Ser
a mostrado agora como as classes e instancias sao declaradas. Por exemplo, a
classe Visible que transforma cada valor em uma String e d
a a ele um tamanho:
class Visible t where
toString :: t -> String
size :: t -> Int
A denicao inclui o nome da classe (Visible) e uma assinatura, que sao as funcoes que
compoem a classe juntamente com seus tipos. Um tipo t para pertencer a` classe Visible tem
de implementar as duas funcoes da assinatura, ou seja, coisas visveis sao coisas que podem ser
transformadas em uma String e que tenham um tamanho. Para declarar o tipo Char como
uma inst
ancia da classe Visible, devemos fazer a declaracao
instance Visible Char where
toString ch = [ch]
size _ = 1
que mostra como um caractere pode ser transformado em uma String de tamanho 1. De forma
similar, para declararmos o tipo Bool como uma instancia da classe Visible, temos de declarar
instance
toString
toString
size _ =
5.2.4
Classes derivadas
Uma classe pode herdar as propriedades de outras classes, como nas linguagens orientadas a
objetos. Como exemplo, vamos observar a classe Ord que possui as operacoes >, >=, <, <=,
max, min e compare, alem de herdar a operacao == da classe Eq. Sua denicao e feita da
seguinte forma:
class Eq t => Ord a where
(<), (<=), (>), (>=) :: t -> t -> Bool
max, min :: t -> t -> t
compare :: t -> t -> Ordering
O tipo Ordering sera denido mais adiante, quando nos referirmos aos tipos algebricos
de dados. Neste caso, a classe Ord herda as operacoes de Eq (no caso, apenas ==). H
a a
necessidade de se denir pelo menos a funcao < (pode ser outra) para ser utilizada na denicao
das outras funcoes da assinatura. Estas denicoes formam o conjunto de declaracoes a seguir:
112
x <= y = (x < y || x == y)
x > y = y < x
x >= y = (y < x || x == y)
Vamos supor que se deseja ordenar uma lista e mostrar o resultado como uma String.
Podemos declarar a funcao vSort para estas operacoes, da seguinte forma:
vSort = toString . iSort
onde toString e iSort sao funcoes ja denidas anteriormente. Para ordenar a lista e necessario
que ela seja composta de elementos que pertencam a um tipo t que possa ser ordenado, ou seja,
pertenca `a classe Ord. Para converter o resultado em uma String, e necessario que a lista [t]
pertenca `a classe Visible. Desta forma, vSort tem o tipo
vSort :: (Ord t, Visible t) => t -> String
mostrando que t deve pertencer `as classes Ord e Visible. Tais tipos incluem Bool, Char,
alem de outros.
Este caso de restricoes m
ultiplas tambem pode ocorrer em uma declaracao de inst
ancia como:
instance (Eq a, Eq b) => Eq (a, b) where
(x, y) == (z, w) = x == z && y == w
mostrando que se dois tipos a e b estiverem na classe Eq entao o par (a, b) tambem esta. As
restricoes m
ultiplas tambem podem ocorrer na denicao de uma classe. Por exemplo,
class (Ord a, Visible a) => OrdVis a
signicando que os elementos da classe OrdVis herdam as operacoes das classes Ord e Visible.
Este e um caso de declaracao de uma classe que nao contem assinatura, ou contem uma
assinatura vazia. Para estar na classe OrdVis, um tipo deve semplesmente estar nas classes
Ord e Visible. Neste caso, a denicao da funcao vSort anterior poderia ser modicada para
vSort :: OrdVis t => [t] -> String
O caso em que uma classe e construda a partir de duas ou mais classes e chamado de heranca
m
ultipla.
Exerccios
1. Como voce colocaria Bool, o tipo par (a, b) e o tipo tripla (a, b, c) como inst
ancias
do tipo Visible?
2. Dena uma funcao para converter um valor inteiro em uma String e mostre como Int
pode ser uma inst
ancia de Visible.
3. Qual o tipo da funcao compare x y = size x <= size y?
5.2.5
As classes pr
e-definidas em Haskell
Haskell contem algumas classes pre-denidas e vamos ver algumas delas com alguma explanacao
sobre a sua utilizacao e denicao.
A classe Eq
Esta classe, ja descrita anteriormente, contem os tipos para os quais sao denidas funcoes
de igualdade ==. Estas denicoes usam as seguintes denicoes default:
113
class Eq t where
(==), (/=) :: t -> t -> Bool
x /= y = not (x == y)
x == y = not (x /= y)
A classe Ord
Esta e a classe que contem tipos, cujos valores podem ser ordenados. Sua denicao e:
class (Eq t) => Ord t where
compare :: t -> t -> Ordering
(<), (<=), (>=), (>) :: t -> t -> Bool
max, min :: t -> t -> t
onde o tipo Ordering tem tres resultados possveis: LT, EQ e GT, que s
ao os resultados
possveis de uma comparacao entre dois valores. A denicao de compare e:
compare x y
|x == y
|x <= y
|otherwise
= EQ
= LT
= GT
A vantagem de se usar a funcao compare e que muitas outras funcoes podem ser denidas
em funcao dela, por exemplo,
x
x
x
x
<= y
< y
>= y
> y
=
=
=
=
compare
compare
compare
compare
x
x
x
x
y
y
y
y
/=
==
/=
==
GT
LT
LT
GT
-----
[n ..]
[n, m ..]
[n .. m]
[n, n .. m]
As funcoes fromEnum e toEnum tem as funcoes ord e chr do tipo Char como correspondentes, ou seja ord e chr sao denidas usando fromEnum e toEnum. Simon Thompson [35]
arma que o Haskell report estabelece que as funcoes toEnum e fromEnum n
ao sao signicativas para todas as inst
ancias da classe Enum. Para ele, o uso destas funcoes sobre valores de
ponto utuante ou inteiros de precis
ao completa (Integer) resulta em erro de execucao.
A classe Bounded
Esta e uma classe que apresenta um valor mnimo e um valor m
aximo para a classe. Suas
instancias sao Int, Char, Bool e Ordering. Sua denicao e a seguinte:
class Bounded t where
minBound, maxBound :: t
que retornam os valores mnimos e maximos de cada tipo.
A classe Show
Esta classe contem os tipos cujos valores podem ser escritos como String. A maioria dos
tipos pertencem a esta classe. Sua denicao e a seguinte:
type ShowS = String -> String
class Show a where
showsPrec :: Int -> a -> ShowS
show :: a -> String
showList :: [a] -> ShowS
A classe Read
Esta classe contem os tipos cujos valores podem ser lidos a partir de strings. Para usar a
classe e necessario apenas conhecer a funcao read :: Read t => String > t. Esta classe
complementa a classe Show uma vez que as strings produzidas por show sao normalmente
possveis de serem lidas por read.
read :: Read t => String -> t
5.3
Tipos alg
ebricos
J
a foram vistas v
arias formas de modelar dados em Haskell. Vimos as funcoes de alta ordem,
polimorsmo e as classes de tipos (type class) como metodologia para implementar sobrecarga.
Estes dados foram modelados atraves dos seguintes tipos:
Tipos b
asicos (primitivos): Int, Float, Bool, Char e listas.
Tipos compostos: tuplas (t1 , t2 , ..., tn ) e funcoes (t1 > t2 ), onde t1 e t2 sao tipos.
Estas facilidades que a linguagem oferece, por si s
o, j
a mostram o grande poder de expressividade e de abstracao que elas proporcionam. No entanto, outros tipos de dados precisam ser
modelados. Por exemplo,
os meses: janeiro, ..., dezembro;
tipos cujos elementos sejam n
umeros ou strings. Por exemplo, uma casa em uma rua
pode ser identicada por um n
umero ou pelo nome da famlia. (n
ao no Brasil);
o tipo arvore.
115
Para construir um novo tipo de dados, usamos a declaracao data que descreve como os elementos deste novo tipo de dados s
ao construdos. Cada elemento e nomeado por uma expressao
formulada em funcao dos construtores do tipo. Alem disso, nomes diferentes denotam elementos tambem distintos. Atraves do uso de pattern matching sobre os construtores, projetam-se
operacoes que geram e processam elementos do tipo de dados escolhidos pelo programador. Os
tipos assim descritos, nao as operacoes, sao chamados tipos concretos [35, 4] e sao modelados
em Haskell atraves dos tipos algebricos.
5.3.1
Um tipo algebrico e mais uma facilidade que Haskell oferece, proporcionando ao programador
mais poder de abstracao, necessario para modelar algumas estruturas de dados complexas. Outras possibilidades tambem existem, como por exemplo, os tipos abstratos de dados a serem
vistos mais adiante. A sintaxe de uma declaracao de um tipo algebrico de dados e
data <Nome_do_tipo> = <Const1> | <Const2> | ...
| <Constn>
Podemos formar uma funcao mostraPessoa que toma um elemento do tipo Gente e o mostra
na tela:
mostraPessoa :: Gente -> String
mostraPessoa (Pessoa n a) = n ++ " -- " ++ show a
Neste caso, a aplicacao da funcao mostraPessoa (Pessoa John Lennon, 60) sera
respondida por
>"John Lennon -- 60"
Neste caso, o tipo tem um u
nico construtor, Pessoa, que tem dois elementos, Nome e
Idade, para formar o tipo Gente. Nos tipos enumerados, Tempo e Estacao, os construtores
sao nul
arios (0-arios) porque n
ao tem argumentos. Assim, Pessoa n a pode ser interpretado
como sendo o resultado da aplicacao da funcao Pessoa aos argumentos n e a, ou seja, Pessoa
:: Nome > Idade > Gente.
Uma outra denicao de tipo para Gente poderia ser
type Gente = (Nome, Idade)
Existem vantagens e desvantagens nesta nova vers
ao, no entanto, e senso comum que ela deve ser
evitada. Podemos usar o mesmo nome para o tipo e para o construtor, no entanto, esta escolha
pode conduzir a ambig
uidades e, portanto, deve ser evitada. Por exemplo, e perfeitamente legal
a construcao do tipo data Pessoa = Pessoa Nome Idade.
Exemplo. Uma forma geometrica pode ser um crculo ou um ret
angulo. Ent
ao podemos
modela-la da seguinte maneira:
data Forma = Circulo Float | Retangulo Float Float
Agora podemos declarar funcoes que utilizam este tipo de dados, por exemplo, a funcao que
calcula a area de uma forma geometrica:
area :: Forma -> Float
area (Circulo r) = pi * r * r
area (Retangulo h w) = h * w
5.3.2
A forma geral
Os exemplos motrados indicam que um tipo algebrico e declarado com a seguinte sintaxe:
data <NomedoTipo> =
5.3.3
Derivando inst
ancias de classes
Ao se introduzir um tipo algebrico, como Estacao, podemos desejar que ele tambem tenha
igualdade, enumeracao, etc. Isto pode ser feito pelo sistema, informando que o tipo tem as
mesmas funcoes que as classes Eq, Ord e Show tem:
data
Exerccios:
1. Redena a funcao temperatura :: Estacao > Tempo de forma a usar guardas em
vez de pattern matching. Qual denicao deve ser preferida, em sua opini
ao?
2. Dena o tipo Meses como um tipo algebrico em Haskell. Faca uma funcao que associe
um mes `a sua estacao. Coloque ordenacao sobre o tipo.
3. Dena uma funcao que de o tamanho do permetro de uma forma geometrica do tipo
Forma.
4. Adicione um construtor extra ao tipo Forma para tri
angulos e estenda as funcoes area e
perimetro (exerccio anterior) para incluir os tri
angulos.
5. Dena uma funcao que decida quando uma forma e regular. Um crculo e regular, um
quadrado e um retangulo regular e um tri
angulo equilatero e regular.
5.3.4
Tipos recursivos
:: Expr
(Lit n)
(Add e1
(Sub e1
-> Int
= n
e2) = (eval e1) + (eval e2)
e2) = (eval e1) - (eval e2)
Esta denicao e primitiva recursiva, ou seja, existe uma denicao para um caso base (Lit n)
e uma denicao recursiva para os casos indutivos. Por exemplo, uma funcao que imprime uma
expressao pode ser feita da seguinte maneira:
mostraExpressao :: Expr -> String
mostraExpressao (Lit n) = show n
118
Arvores bin
arias de inteiros
Uma arvore bin
aria ide inteiros ou e nula ou composta de n
os internos e externos. Cada n
o
ou e a arvore nula ou e um n
umero inteiro com duas sub-
arvores bin
arias de inteiros como seus
descendentes. Para os n
os internos, as duas sub-
arvores n
ao podem ser ambas nulas e para as
folhas elas sao nulas. Uma maneira de se modelar estas arvores em Haskell e simplesmente
data ArvoreInt = Nil | No Int ArvoreInt ArvoreInt
As arvores bin
arias de inteiros podem ser mostradas gracamente, conforme a Figura 5.1.
numOcorrencias
numOcorrencias
|n == p
=
|otherwise =
5.3.5
Nil p = 0
(No n t1 t2) p
1 + (numOcorrencias t1 p) + (numOcorrencias t2 p)
(numOcorrencias t1 p) + (numOcorrencias t2 p)
Recurs
ao m
utua
Uma propriedade muito difcil de ser implementada em qualquer linguagem de programacao, seja
imperativa ou pertencente a outro paradigma, e recursao m
utua. Por este motivo, a maioria das
linguagens n
ao permite esta facilidade. No entanto, Haskell a oferece, dando um maior poder de
abstracao ao programador. Observamos, no entanto, que este tipo de recurs
ao deve ser utilizado
com cuidado para evitar erros e/ou ambiguidades. Um exemplo pode ser
data Pessoa = Adulto Nome Endereco Biografia
| Crianca Nome
data Biografia = Pai String [Pessoa] | NaoPai String
mostraPessoa (Adulto nom end bio)
= mostraNome nom ++ mostraEndereco end ++ mostraBiografia bio
. . .
mostraBriografia (Pai st listaPes)
= st ++ concat (map mostraPessoa listaPes)
Exerccios:
1. Calcule:
eval (Lit 67)
eval (Add (Sub (Lit 3) (Lit 1) (Lit 3))
mostraExpressao (Add (Lit 67) (Lit (-34))).
2. Adicione as operacoes de divisao e multiplicacao de inteiros ao tipo Expr e re-dena as
funcoes eval, showExpr e size (que d
a a quantidade de operadores em uma expressao).
O que sua operacao de divis
ao faz no caso da divis
ao ser por zero?
3. Calcule: somaArvInt (No 3 (No 4 Nil Nil) Nil) e
(No 4 Nil Nil) Nil), passo a passo.
profundidade (No 3
5.3.6
Tipos alg
ebricos polim
orficos
As listas, j
a vistas anteriormente, podem ser construdas a partir de tipos algebricos.
data List t = Nil | Cons t (List t)
deriving (Eq, Ord, Show)
Arvores
bin
arias em geral
As arvores bin
arias vistas anteriormente sao arvores bin
arias de inteiros. Mas podemos
construir arvores bin
arias onde seus nos sejam de um tipo vari
avel.
data Arvore t = Nil | No t (Arvore t) (Arvore t)
deriving (Eq, Ord, Show)
As funcoes que manipulam arvores bin
arias de inteiros devem ser re-denidas para tratar
com esta nova estrutura, apesar das diferencas serem mnimas. Com relacao a` profundidade
deve-se observar que esta funcao se refere a cada no e n
ao a uma arvore como um todo. Dito de
outra forma, cada n
o de uma arvore tem uma profundidade diferente, a n
ao ser que eles estejam
no mesmo nvel. Assim, para encontrar a profundidade de cada n
o em uma arvore de algum tipo
t, e necessario fazer uma transformacao desta arvore em uma arvore de tuplas onde o primeiro
elemento e um valor do tipo t e o segundo e a profundidade do n
o. Se a arvore em questao
for constituda de inteiros, Arvore Int, uma outra possibilidade e transformar esta arvore de
inteiros em uma arvore de lista de inteiros. Para o caso de transformacao em uma arvore de
tuplas, a denicao pode ser feita da seguinte forma:
transforma :: Arvore t -> Int -> Arvore (t,Int)
transforma Nil n
= Nil
transforma (No n t1 t2) prof = No (n, prof) (transforma t1 (prof+1))
(transforma t2 (prof+1))
arvBinInttoLista :: Arvore a -> Arvore (a, Int)
arvBinInttoLista arv = transforma arv 0
Uma funcao que transforme uma arvore em uma lista pode ser construda facilmente da
seguinte forma:
transf :: Arvore t -> [t]
transf Nil = [ ]
transf (No x t1 t2) = transf t1 ++ [x] ++ transf t2
transf (No 12 (No 34 Nil Nil) (No 3 (No 17 Nil Nil) Nil)) = [34, 12, 17, 3]
Uma funcao que aplica uma outra funcao a todos os elementos de uma arvore transformandoa em outra arvore, fazendo o papel de mapeamento, pode ser denida como segue:
mapArvore :: (t -> u) -> Arvore t -> Arvore u
mapArvore f Nil = Nil
mapArvore f (No x t1 t2) = No (f x ) (mapTree f t1) (mapArvore f t2)
O tipo uni
ao
As denicoes tambem podem tomar mais de um parametro e podemos formar um tipo cujos
elementos sejam de um tipo t ou de um tipo u.
data Uniao t u = Nome t | Numero u
122
5.4
Tratamento de erros
Para se construir bons programas, e necessario que especicar o que o programa deve fazer
no caso de acontecer algumas situacoes anomalas. Estas situacoes anomalas sao chamadas de
excecoes e, entre elas, podemos citar:
tentativa de divis
ao por zero, calculo de raiz quadrada de n
umero negativo ou a aplicacao
da funcao fatorial a um n
umero negativo, entre outros,
tentativa de encontrar a cabeca ou a cauda de uma lista vazia.
Nesta secao, este problema e analisado atraves de tres tecnicas. A solucao mais simples e
exibir uma mensagem sobre o motivo da ocorrencia e parar a execucao. Isto e feito atraves de
uma funcao de erro.
error :: String -> t
Uma tentativa de avaliar a express
ao error Circulo com raio negativo.resultaria na mensagem
Program error: Circulo com raio negativo.
que seria impressa e a execucao do programa terminaria.
O problema com esta tecnica e que todas as informacoes usuais da computacao sao perdidas,
porque o programa e abortado. Em vez disso, o erro pode ser tratado de alguma forma, sem
parar a execucao do programa. Isto pode ser feito atraves das duas tecnicas a seguir.
5.4.1
Valores fictcios
Para estes dois casos, a escolha dos valores ctcios e obvia. No entanto, existem casos em
que esta escolha nao e possvel. Por exemplo, na denicao da cabeca de uma lista. Neste caso,
qual seria um valor ctcio adequado? Para resolver esta situacao, temos de recorrer a um
artifcio mais elaborado. Precesamos re-denir a funcao hd, acrescentando mais um par
ametro.
hd :: a -> [a] -> a
hd y (x:_) = x
hd y [ ]
= y
Esta tecnica e mais geral e consiste na denicao de uma nova funcao para o caso de ocorrer
erro. Neste caso, a funcao de um argumento foi modicada para ser aplicada a dois argumentos.
De uma forma geral, temos de construir uma funcao com uma denicao para o caso da excecao
acontecer e outra denicao para o caso em que ela nao aconteca.
fErr y x
|cond
= y
|otherwise = f x
Esta tecnica funciona bem em muitos casos. O u
nico problema e que n
ao e reportada
nenhuma mensagem informando a ocorrencia incomum. Uma outra abordagem e processar a
entrada indesej
avel.
5.4.2
Tipos de erros
5.5
Com os tipos algebricos tambem podemos realizar provas sobre a corretude ou nao de alguns
programas, de forma similar `as provas com outros tipos de dados. Por exemplo, reformulando a
denicao de
arvore bin
aria, podemos ter
data Arvore t = Nil | No t (Arvore t) (Arvore t)
deriving (Eq, Ord, Show)
Para provar uma propriedade P(tr) para todas as arvores nitas do tipo Arvore t, temos
de analisar dois casos:
1. o caso Nil: vericar se P(Nil) e verdadeira e
2. o caso No: vericar se P(No x tr1 tr2) para todo x, assumindo P(tr1) e P(tr2).
Exemplo: provar que map f (collapse tr) = collapse (mapTree f tr).
Para provar isto devemos usar as denicoes anteriores:
map f [ ] = [ ]
map (a : x) = f a : map f x
(1)
(2)
(3)
collapse Nil = [ ]
collapse (No x t1 t2 )
= collapse t1 ++ [x] ++ collapse t2
(5)
(4)
(6)
Esquema da prova:
O caso Nil:
Lado esquerdo
map f (collapse Nil)
= map f [ ]
=[]
por (5)
por (1)
Lado direito
collapse (mapTree f Nil)
= collapse Nil
=[]
por (3)
por (5)
Lado esquerdo
map f (collapse(No x tr1 tr2))
=map f (collapse tr1 ++ [x] ++
collapse tr2)
=map f (collapse tr1) ++ [f x] ++
map f (collapse tr2)
=collapse (mapTree f tr1) ++ [f x]
++ collapse(mapTree f tr2)
por (6)
por (9)
Lado direito
collapse (mapTree f (No x tr1 tr2))
= collapse (No (f x)
(mapTree f tr1) (mapTree f tr2))
por (4)
por (6)
por (7 e 8)
5.6
M
odulos em Haskell
No Captulo introdut
orio desta Apostila, armamos que a principal vantagem das linguagens
estruturadas era a modularidade oferecida por elas e, por este motivo, as linguagens funcionais
eram indicadas como solucao para a crise do software dos anos 80. De fato, as linguagens
funcionais s
ao modulares e Haskell oferece muitas alternativas para a construcao de modulos,
obedecendo `as exigencias preconizadas pela Engenharia de Software para a construcao de programas.
Para John Hughes [15], a modularidade e a caracterstica que uma linguagem de programacao
deve apresentar para que seja utilizada com sucesso. Ele arma que esta e a caracterstica principal das linguagens funcionais, proporcionada atraves das funcoes de alta ordem e do mecanismo
de avaliacao lazy.
A modularidade e importante porque:
as partes de um sistema podem ser construdas separadamente,
as partes de um sistema podem ser compiladas e testadas separadamente e
pode-se construir grandes bibliotecas para utilizacao.
No entanto alguns cuidados devem ser tomados para que estes benefcios sejam auferidos,
caso contrario, em vez de facilitar pode complicar a sua utilizacao. Entre os cuidados que devem
ser levados em consideracao podem ser citados:
cada modulo deve ter um papel claramente denido,
cada modulo deve fazer exatamente uma u
nica tarefa,
cada modulo deve ser auto-contido,
cada modulo deve exportar apenas o que e estritamente necessario e
os modulos devem ser pequenos.
128
5.6.1
Cabe
calho em Haskell
5.6.2
Importa
c
ao de m
odulos
Os modulos em Haskell podem importar dados e funcoes de outros modulos da seguinte forma:
module Abelha where
import Formiga
pegadordeAbelha = . . .
As denicoes visveis em Formiga podem ser utilizadas em Abelha.
module Vaca where
import Abelha
Neste caso, as denicoes Formiga e comeFormiga n
ao sao visveis em Vaca. Elas podem
se tornar visveis pela importacao explcita de Formiga ou usando os controles de exportacao
para modicar o que e exportado a partir de Abelha.
5.6.3
O m
odulo main
5.6.4
Controles de exportac
ao
Por default, tudo o que e declarado em um modulo pode ser exportado, e apenas isto, ou seja, o
que e importado de outro m
odulo n
ao pode ser exportado pelo m
odulo importador. Esta regra
pode ser exageradamente permissiva porque pode ser que algumas funcoes auxiliares n
ao devam
ser exportadas e, por outro lado, pode ser muito restritiva porque pode existir alguma situacao
em que seja necessario exportar algumas denicoes declaradas em outros modulos.
Desta forma, deve-se poder controlar o que deve ou n
ao ser exportado. Em Haskell, isto e
declarado da seguinte forma: module Abelha (pegadorAbelha, Formigas(. . .), comeFormiga) where . . . ou equivalentemente module Abelha (module Abelha, module
Formiga) where . . .
A palavra module dentro dos parenteses signica que tudo dentro do m
odulo e exportado,
ou seja, module Abelha where e equivalente a module Abelha (module Abelha) where.
129
5.6.5
Controles de importa
c
ao
Da mesma forma que Haskell tem controles para exportacao, tem tambem controles para a
importacao.
module Tall where
import Formiga (Formigas (. . .))
Neste caso, a intencao e importar apenas o tipo Formigas do m
odulo Formiga. Pode-se
tambem esconder alguma entidade. Por exemplo,
module Tall where
import Formiga hiding (comeFormiga)
Nesta situacao, a intencao e esconder a funcao comeFormiga. Se em um modulo existir
um objeto com o mesmo nome de outro objeto, denido em um m
odulo importado, pode-se
acessar ambos objetos usando um nome qualicado. Como exemplo, Formiga.urso e o objeto
importado e urso e o objeto denido localmente. Um nome qualicado e construdo a partir
do nome de um m
odulo e do nome do objeto neste m
odulo. Para usar um nome qualicado, e
necessario que se faca uma importacao:
import qualified Formiga
No caso dos nomes qualicados, pode-se tambem estabelecer quais tens vao ser exportados
e quais serao escondidos. Tambem e possvel usar um nome local para um m
odulo importado,
como em
import Inseto as Formiga
Nesta secao zemos um estudo rapido sobre a utilizacao de modulos em Haskell. O objetivo
foi apenas mostrar as muitas possibiolidades que a linguagem oferece. A pr
atica da programacao
de modulos e que vai dotar o programador da experiencia e habilidade necessarias para o desenvolvimento de programas. A pr
oxima secao e dedicada aos tipos abstratos de dados, cujo
domnio, prescinde da pr
atica da programacao e deve ser o metodo de estudo a ser adotado pelo
leitor.
5.7
O objetivo desta secao e introduzir os tipos abstratos de dados (TAD) e o mecanismo provido por
Haskell para den-los. De maneira geral, os tipos abstratos de dados diferem dos introduzidos
por uma declaracao data, no sentido de que os TADs podem escolher a representacao de seus
valores. Cada escolha de representacao conduz a uma implementacao diferente do tipo de dado
[35].
Os tipos abstratos sao denidos de forma diferente da forma utilizada para denir os tipos
algebricos. Um tipo abstrato n
ao e denido pela nomeacao de seus valores, mas pela nomeacao de
suas operacoes. Isto signica que a representacao dos valores dos tipos abstratos n
ao e conhecida.
O que e realmente de conhecimento p
ublico e o conjunto de funcoes para minipular o tipo. Por
exemplo, Float e um tipo abstrato em Haskell. Para ele, s
ao exibidas operacoes de comparacao
e aritmeticas alem de uma forma de exibicao de seus valores, mas nao se estabelece como tais
n
umeros sao representados pelo sistema de avaliacao de Haskell, proibindo o uso de casamento
130
de padr
oes. Em geral, o programador que usa um tipo abstrato n
ao sabe como seus elementos
sao representados. Tais barreiras de abstracao sao usuais quando mais de um programador
estiverem trabalhando em um mesmo projeto ou mesmo quando um mesmo programador estiver
trabalhando em um projeto n
ao trivial. Isto permite que a representacao seja trocada sem afetar
a validade dos scripts que usam o tipo abstrato. Vamos mostrar estes fundamentos atraves de
exemplos.
5.7.1
Vamos dar incio ao nosso estudo dos tipos abstratos de dados atraves da implementacao do
tipo Pilha. As pilhas s
ao estruturas de dados homogeneas onde os valores sao colocados e/ou
retirados utilizando uma estrategia LIFO (Last In First Out). Informalmente, esta estrutura
de dados e comparada a uma pilha de pratos na qual s
o se retira um prato por vez e sempre o
que est
a no topo da pilha. Tambem so se coloca um prato por vez e em cima do prato que se
encontra no topo.
As operacoes necessarias para o funcionamento de uma pilha do tipo Stack t1 sao as seguintes:
push
pop
top
stackEmpty
newStack
::
::
::
::
::
t -> Stack
Stack t ->
Stack t ->
Stack t ->
Stack t
da pilha
da pilha
da pilha
vazia
Primeira implementa
c
ao para o TAD Pilha
Para se implementar um tipo abstrato de dados em Haskell, deve-se criar um m
odulo para isto.
A criacao de modulos foi um estudo feito na secao anterior. O leitor deve voltar a este tema se
tiver alguma diculdade de entender as declaracoes aqui feitas. Serao feitas duas implementacoes
do tipo pilha. A primeira baseada em um tipo algebrico e a segunda baseada em lista.
module Stack(Stack, push, pop, top, stackEmpty, newStack} where
push
pop
top
stackEmpty
newStack
::
::
::
::
::
t -> Stack
Stack t ->
Stack t ->
Stack t ->
Stack t
t -> Stack t
Stack t
t
Bool
1
Apesar do autor dar preferencia a nomes em Portugues, nestes Exemplos, eles ser
ao descritos com os nomes
com os quais foram implementados, uma vez que muitas implementaco
es j
a est
ao incorporadas ao sistema com
estes nomes e a adoca
o de outros nomes pode provocar confus
ao.
131
top (Stk x _) = x
newStack = EmptyStk
stackEmpty EmptyStk = True
stackEmpty _
= False
instance (Show t) => Show (Stack t) where
show (emptyStk) = #
show (Stk x s) = (show x) ++ | ++ (show s)
A utilizacao do tipo abstrato Stack deve ser feita em outros modulos cujas funcoes necessitam
deste tipo de dado. Desta forma, o m
odulo Stack deve constar da relacao de modulos importados
por este modulo usu
ario. Por exemplo,
module Main where
import Stack
listTOstack :: [t]
-> Stack t
listTOstack [ ]
= new Stack
listTOstack (x : xs) = push x (listTOstack xs)
stackTOlist :: Stack t -> [t]
stackTOlist s
|stackEmpty s = [ ]
|otherwise
= (top s) : (stackTOlist (pop s))
ex1 = push 14 (push 9 (push 18 (push 26 newStack)))
ex2 = push Dunga (push Constantino)
Estes script deve ser salvo em um arquivo que deve ser carregado para ser utilizado. A
implementacao mostrada foi baseada no tipo algebrico Stack t. O implementador pode encontrar uma forma alternativa de implementacao que seja mais eciente que esta e promover esta
mudanca sem que os usuarios saibam disto. O que n
ao pode ser modicada e a Interface com
o usu
ario, para que os programas que j
a utilizam o tipo com a implementacao antiga possam
continuar funcionando com a nova implementacao, sem qualquer modicacao no programa.
Segunda implementa
c
ao para o TAD pilha
Vamos agora mostrar uma segunda implementacao do tipo abstrato pilha baseada em listas. As
operacoes serao as mesmas, para facilitar o entendimento e a comparacao com a implementacao
anterior.
module Stack(Stack, push, pop, top, stackEmpty, newStack} where
push
pop
top
stackEmpty
newStack
::
::
::
::
::
t -> Stack
Stack t ->
Stack t ->
Stack t ->
Stack t
t -> Stack t
Stack t
t
Bool
132
5.7.2
::
::
::
::
::
t -> Queue
Queue t ->
Queue t ->
Queue t ->
Queue t
A lista de operacoes a serem providas pelo TAD, juntamente com seus tipos, constituem a assinatura do TAD, no caso, Queue t. Vamos continuar o exemplo provendo duas implementacoes
do tipo abstrato.
Primeira implementa
c
ao para o TAD Queue
A primeira implementacao a ser mostrada e baseada nas listas nitas. As implementacoes das
operacoes sao as seguintes:
module Queue (Queue, enqueue, dequeue, front, queueEmpty, newQueue) where
enqueue
:: t -> Queue t -> Queue t
133
dequeue
front
queueEmpty
newQueue
::
::
::
::
Queue
Queue
Queue
Queue
t -> Queue t
t -> t
t -> Bool
t
Segunda implementa
c
ao do TAD Queue
A segunda implementacao do tipo la leva em consideracao o desempenho da implementacao.
Podemos construir um modulo, Queue, da seguinte forma:
module Queue (Queue, enqueue, dequeue, queueEmpty, newQueue) where
enqueue
:: t -> Queue t -> Queue t
dequeue
:: Queue t -> Queue t
queueEmpty :: Queue t -> Bool
newQueue
:: Queue t
data Queue t = Fila [t]
newQueue = (Fila [ ])
queueEmpty (Fila [ ]) = True
queueEmpty _
= False
enqueue x (Fila q) = Fila (q ++ [x])
dequeue q@(Fila xs)
|not (queueEmpty q) = (head q, Fila (tail q))
|otherwise
= error A fila estah vazia
instance (Show t) => Show (Queue t) where
show (Fila [ ])
= .
show (Fila (x : xs)) = < ++ (show x) ++ (show (Fila xs))
A funcao dequeue retorna um par: o tem removido da la e a la restante, caso ela n
ao
esteja vazia. Se a la estiver vazia, uma mensagem de erro deve ser chamada para anunciar esta
excecao.
A denicao de dequeue usa um aspecto do casamento de padr
oes que ainda n
ao foi analisado
que e o padr
ao q@(Fila xs). O smbolo @pode ser traduzido por como, no casamento da
entrada. A vari
avel q casa com a entrada completa, Fila xs, de forma que xs d
a acesso `a lista
a partir da qual ela est
a sendo construda. Isto signica que podemos nos referir diretamente a`
entrada completa e a seus componentes na denicao. Sem esta exibilidade, a alternativa seria
dequeue (Fila xs)
|not (queueEmpty (Fila xs)) = (head xs, Fila (tail xs))
|otherwise
= error "A fila esta vazia"
onde a la original seria reconstruda a partir de xs. Esta forma de declaracao implica na
replicacao de xs, de tail xs de tail (tail xs), uma vez que, nas linguagens funcionais, n
ao
existem atualizacoes de celulas, ou seja, todas as vari
aveis sao, na realidade, constantes, porque
seus valores nao podem ser modicados. Isto signica que se x for declarado como tendo o valor
20, ele tera este mesmo valor ate o nal da execucao. Se outro valor for necessario, uma outra
vari
avel sera declarada contendo tal valor. Neste caso, xs tera que ser replicada porque outros
ponteiros podem existir para ela. Segundo [30], este e um problema das linguagens funcionais
e, na maioria dos casos, inevitavel.
O uso do padr
ao @ permite algum compartilhamento, evitando o desperdcio de memoria
com copias desnecessarias. Neste caso, a variavel q e acessada e ela e implementada como um
ponteiro para Fila xs, necessitando apenas celulas para q e n
ao para Fila xs.
135
Otimizando a implementa
c
ao
Em vez de adicionar elementos ao nal da lista, podemos adiciona-los no incio. Esta decis
ao
n
ao requer qualquer alteracao nas denicoes de newQueue e queueEmpty, mas teremos de
redenir enqueue e dequeue. Por exemplo,
enqueue x (Fila xs) = Fila (x : xs)
dequeue q@(Fila xs)
|not (queueEmpty q)
|otherwise
onde as funcoes last e init sao pre-denidas em Haskell. A funcao last retorna o u
ltimo elemento
de uma lista e init retorna a lista inicial, sem seu u
ltimo elemento.
Do ponto de vista da complexidade, vericamos que a funcao enqueue adicionava um elemento ao nal da lista e agora passou a faze-lo na cabeca. Era carae cou barata porque
tinha de percorrer toda a lista para adicionar o elemento ao seu nal, ou seja, dependia do
tamanho da lista. Por outro lado, com a funcao dequeue aconteceu exatamente o contrario:
era baratae cou cara, pelo mesmo motivo.
Uma otimizacao importante que se pode realizar nesta implementacao consiste em implementar as las atraves de duas listas: uma para se retirar e a outra para se adicionar elementos.
A adicao de elementos e feita na cabeca da segunda lista (barata) e a remocao e feita, tambem
na cabeca (tambem barata), mas da primeira lista. Este esquema funciona desta maneira ate
que a primeira lista n
ao seja vazia. No momento em que ela se tornar vazia, mas ainde existirem
elementos na segunda lista, a primeira lista passa a ser a segunda com seus elementos em ordem
inversa e a segunda lista passa a ser a lista vazia. Neste caso, pode-se agora continuar o processo
de retirada de elementos da la, ate que as duas listas se tornem vazias. A nova implementacao
ca da seguinte forma:
data Queue a = Fila [a] [a]
emptyQ = Fila [ ] [ ]
queueEmpty (Fila [ ] [ ]) = True
queueEmpty _
= False
enqueue x (Fila xs ys) = Fila xs (x : ys)
dequeue (Fila (x : xs) ys)
dequeue (Fila [ ] ys)
dequeue (Fila [ ] [ ])
5.7.3
O tipo abstrato Set e uma colecao homogenea de elementos e implementa a nocao de conjunto,
de acordo com a seguinte Interface:
emptySet :: Set t
setEmpty
inSet
addSet
delSet
pickSet
::
::
::
::
::
Set
(Eq
(Eq
(Eq
Set
t -> Bool
t) => t -> Set t -> Bool
t) => t -> Set t -> Set t
t) => t -> Set t -> Set t
t -> t
::
::
::
::
::
::
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)
pickSet (S [ ])
= error conjunto vazio
pickSet (S (x : _)) = x
5.7.4
Uma tabela, Table a b, e uma colecao de associacoes entre chaves, do tipo a, com valores do
tipo b, implementando assim, uma funcao nita, com domnio em a e co-domnio b, atraves de
uma determinada estrutura de dados.
O tipo abstrato Table pode ter a seguinte implementacao:
137
(6666,Margarida,26)]
Neste ponto, encerramos nosso estudo sobre os tipos abstratos de dados. Foram mostrados
v
arios exemplos de implementacao e esperamos ter dado uma ideia ampla da forma como estes
tipos podem ser implementadoe em Haskell. Na realidade os princpios subjacentes e que d
ao
suporte aos tipos abstratos de dados devem ser do domnio de todo programador, em qualquer
paradigma de programacao, principalmente o de orientacao a objetos, ultimamente tao em moda.
O leitor deve praticar a implementacao de exemplos nao mostrados mas que podem ser baseados
neles. Como sugestao, indicamos a implementacao de arvores AVL, Red-black, B, B+, B* entre
outras.
5.8
Lazy evaluation
--linha
--linha
--linha
--linha
--linha
--linha
--linha
1
2
3
4
5
6
7
Deve-se observar que, nas linhas 2 e 3 deste script, a subexpressao (9 - 3) ocorre duas vezes. Neste
caso, o avaliador de Haskell avalia esta subexpress
ao apenas uma vez e guarda este resultado
na expectativa de que ele seja novamente referenciado. Na segunda ocorrencia da mesma subexpressao, ela j
a esta avaliada. Outra caracterstica importante e que o sistema de avaliacao so
vai realizar um calculo se ele for realmente necessario. Como no -calculo, a ordem de avaliacao
e sempre da esquerda para a direita, ou seja, leftmost-outermost. Isto tem importancia na
implementacao: os calculos so serao realizados se realmente forem necessarios e no momento em
que forem solicidatos. Veja que na avaliacao da funcao eqFunc2, a seguir, o segundo argumento
(eqFunc1 34 3) n
ao e avaliado, uma vez que ele n
ao e necessario para a aplicacao da funcao
eqFunc2.
eqFunc2 a b
eqFunc2 (10
= (10 *
= 400 +
= 432
= a + 32
* 40) (eqFunc1 34 3)
40) + 32
32
Ser
ao vistas, a seguir, algumas implicacoes que esta forma de avaliacao tem sobre a construcao
de listas usando compreenssoes e sobre a construcao de listas potencialmente innitas.
139
5.8.1
Express
oes ZF (revisadas)
Ha uma forte interdependencia entre a criacao de listas por compreensao e o sistema de avaliacao
preguicosa de Haskell. Algumas aplicacoes que usam a construcao de listas por compreensao
so podem ser completamente entendidas se conhecermos o sistema de avaliacao, principalmente
quando envolver a criacao de listas potencialmente innitas.
Uma express
ao ZF tem a seguinte sintaxe:
[e | q1 , ..., qk ], onde cada qi e um qualicador e tem uma entre duas formas:
1. um gerador: p<- lExp, onde p e um padr
ao e lExp e uma expressao do tipo lista ou
2. um teste: bExp, onde bExp e uma expressao booleana, tambem conhecida como guarda.
Exemplos
pares :: [t] -> [u] -> [(t,u)]
pares l m = [(a,b) | a <- l, b <- m]
pares [1,2,3] [4,5] = [(1,4), (1,5), (2,4), (2,5), (3,4), (3,5)]
trianRet :: Int -> [(Int, Int, Int)]
trianRet n = [(a,b,c) | a <- [2..n], b <- [a+1..n], c <- [b+1..n],
a*a + b*b = c*c ]
triRetan 100 = [(3,4,5), (5,12,13), (6,8,10), ..., (65,72,97)]
Deve ser observado, neste u
ltimo exemplo, a ordem em que as triplas foram geradas. Ele pega o
primeiro n
umero do primeiro gerador, em seguida o primeiro gerador do segundo, depois percorre
todos os valores do terceiro gerador. No momento em que os elementos do terceiro gerador se
exaurirem o mecanismo volta para o segundo gerador e pega agora o seu segundo elemento e vai
novamente percorrer todo o terceiro gerador. Ap
os se exaurirem todos os elementos do segundo
gerador ele volta agora para o segundo elemento do primeiro gerador e assim prossegue ate o
nal.
Seq
u
encia de escolhas
Para mostrar a seq
uencia de atividades, vamos utilizar a notacao do -calculo para reducoes. Seja e uma expressao, portanto e{f/x} e a expressao e onde as ocorrencias de x
sao substitudas por f. Formalmente, temos:
[e | v < [a1 , ..., an ], q2 , ..., qk ]
= [e{a1 /v}, q2 {a1 /v}, ..., qk {a1 /v}] ++ ... ++ [e{an /v} | q2 {an /v}, ..., qk {an /v}].
Por exemplo, temos:
[(a, b) | a < l]{[2, 3]/l} = [(a, b) | a < [2, 3]] e
(a + sumx){(2, [3, 4])/(a, x)} = 2 + sum[3, 4].
Regras de teste
Na utilizacao desta sintaxe e necessario que algumas regras de aplicacao sejam utilizadas.
Estas regras s
ao importantes para entender porque algumas aplicacoes, envolvendo a criacao de
listas, n
ao chegam aos resultados pretendidos.
[e | T rue, q2 , ..., qk ] = [e | q2 , ..., qk ],
[e | F alse, q2 , ..., qk ] = []
[e |] = [e].
Exemplos
Vamos mostrar a seq
uencia de operacoes para alguns exemplos para car mais claro:
140
5.8.2
5.8.3
Listas infinitas
Listas innitas nao tem sido utilizadas na maioria das linguagens de programacao. Mesmo nas
linguagens funcionais, elas s
o sao implementadas em linguagens que usam lazy evaluation. Na
realidade, estas listas nao sao innitas e sim sao potencialmente innitas. Vejamos um exemplo.
uns :: [Int]
uns = 1 : uns
5.9
Resumo
A grande fonte de exemplos e exerccios mostrados neste Captulo, foram os livros de Simon
Thompson [35] e Richard Bird [4]. No entanto, um fonte importante de problemas a serem
resolvidos podem ser o livro de Steven Skiena [33] e os sites:
http://www.programming-challenges.com e
http://online-judge.uva.es.
As descricoes de arvores AVL, B, B+, B*, red-black e outros tipos podem ser encontradas
na bibliograa dedicada aos temas Algoritmos e Estruturas de Dados. O leitor e aconselhado a
consultar.
Para quem deseja conhecer mais aplicacoes de programas funcionais, o livro de Paul Hudak
[14] e uma excelente fonte de estudo, principalmente para quem deseja conhecer aplicacoes da
multimdia usando Haskell.
143
144
Captulo 6
Programa
c
ao com a
c
oes em Haskell
... for exemple, a computation implying the modication of
a state for keeping track of the number of evaluatin steps
might be described by a monad which takes as input parameter and
returns the new state as part of its result.
Computations raising exceptions or performing input-output can
also be described by monads.
(Fethi Rabhi et Guy Lapalme in [30])
6.1
Introdu
c
ao
Este Captulo e dedicado a` forma utilizada por Haskell para se comunicar com o mundo exterior,
ou seja, para fazer operacoes de I/O. Isto se faz necessario porque, para o paradigma funcional,
os programas sao expressoes que sao avaliadas para se encontrarem valores que sao atribudos
a nomes. Para Haskell, o resultado de um programa e o valor de nome main no M
odulo Main
do arquivo Main.hs. No entanto, os valores dos nomes sao imut
aveis durante a execucao do
programa, ou seja, o paradigma funcional n
ao admite atribuicoes destrutivas.
Mas a realidade e que a grande maioria dos programas exige alguma interacao com o mundo
externo. Por exemplo:
um programa pode necessitar ler alguma entrada de algum terminal ou escrever neste ou
em outro terminal,
um sistema de e-mail le e escreve em arquivos ou em canais e
um programa pode querer mostrar uma gura em uma janela do monitor.
Historicamente, as operacoes de I/O representaram um desao muito grande durante muito
tempo para os usu
arios das linguagens funcionais. Algumas delas tomaram rumos distintos na
solucao destes problemas. Por exemplo, Standard ML [27] preferiu incluir operacoes como
inputInt :: Int
cujo efeito e a leitura de um valor inteiro a partir do dispositivo padr
ao de entrada. Este
valor lido e atribudo a inputInt. Mas surge um problema porque, cada vez que inputInt e
avaliado, um novo valor e a ele tribudo. Esta e uma caracterstica do paradigma imperativo,
n
ao do modelo funcional. Por este motivo, diz-se que SML admite um modelo funcional impuro,
porque admite atribuicoes destrutivas.
Seja a seguinte denicao de uma funcao, em SML, que calcula a diferenca entre dois inteiros:
145
6.2
Como ja descrito, um programa funcional consiste em uma expressao que e avaliada para encontrar um valor que ser
a ligado a um identicador. No caso de uma operacao de IO, que valor
deve ser retornado? Por exemplo, em uma operacao de escrita de um valor na tela do monitor,
que valor deve ser retornado? Este retorno e necessario para que o paradigma seja obedecido.
Caso contrario, ele e corrompido.
A solucao adotada pelos idealizadores de Haskell foi introduzir um tipo especial chamado
acao. Quando o sistema Haskell detecta um valor deste tipo, ele sabe que uma acao deve ser
executada e nao um calculo para encontrar um valor a ser nomeado. Existem acoes primitivas,
por exemplo escrever um caractere em um arquivo ou receber um caractere do teclado, mas
tambem acoes compostas como imprimir uma string inteira em um arquivo.
As expressoes em Haskell, cujos resultados de suas avaliacoes sejam acoes, sao chamadas de
comandos, porque elas comandam o sistema para realizar alguma acao. As funcoes, cujos
retornos sejam acoes, tambem sao chamadas de comandos. Todos os comandos realizam acoes e
retornam um valor de um determinado tipo T, que pode ser usado, futuramente, pelo programa.
Haskell prove o tipo IO a para permitir que um programa faca alguma operacao de I/O
e retorne um valor do tipo a. Haskell tambem prove um tipo IO () que contem um u
nico
elemento, representado por (). Uma funcao do tipo IO () representa uma operacao de I/O
(acao) que retorna o valor (). Semanticamente, este e o mesmo resultado de uma operacao de
I/O que n
ao retorna qualquer valor. Por exemplo, a operacao de escrever a string Olha eu
aqui!pode ser entendida desta forma, ou seja, um objeto do tipo IO ().
Existem muitas funcoes pre-denidas em Haskell para realizar acoes, alem de um mecanismo
para seq
uencializ
a-las, permitindo que alguma acao do modelo imperativo seja realizada sem
ferir o modelo funcional.
6.2.1
Opera
c
oes de entrada
getChar :: IO Char
De forma similar, para ler uma string, a partir do dispositivo padr
ao de entrada, usamos a
funcao pre-denida getLine do tipo:
getLine :: IO String
As aplicacoes destas funcoes devem ser interpretadas como operacoes de leitura seguidas de
retornosi; no primeiro caso de um caractere e, no segundo, de uma string.
6.2.2
Opera
c
oes de sada
A operacao de impressao de um texto, e feita por uma funcao que toma a string a ser escrita
como entrada, escreve esta string no dispositivo padr
ao de sada e retorna um valor do tipo ().
Esta funcao foi citada no Captulo 3, mas de forma generica e sem nenhuma profundidade, uma
vez que, seria difcil o leitor entender sua utilizacao com os conhecimentos sobre Haskell, ate
aquele ponto, adquiridos. Esta funcao e putStr, pre-denida em Haskell, com o seguinte tipo:
putStr :: String -> IO ()
Agora podemos escrever Olha eu aqui!, da seguinte forma:
aloGalvao :: IO ()
aloGalvao = putStr "Olha eu aqui!"
Usando putStr podemos denir uma funcao que escreva uma linha de sada:
putStrLn :: String -> IO ()
putStrLn = putStr . (++ "\n")
cujo efeito e adicionar o caractere de nova linha ao m da entrada passada para putStr.
Para escrever valores em geral, Haskell prove a classe Show com a funcao
show :: Show a => a -> String
que e usada para transformar valores, de v
arios tipos, em strings para que possam ser mostradas
atraves da funcao putStr. Por exemplo, pode-se denir uma funcao de impressao geral
print :: Show a => a -> IO ()
print = putStrLn . show
Se o objetivo for denir uma acao de I/O que n
ao realize qualquer operacao de I/O, mas
que retorne um valor, pode-se utilizar a funcao
return :: a -> IO a
cujo efeito e n
ao realizar qualquer acao de I/O e retornar um valor do tipo a.
147
6.2.3
A nota
c
ao do
6.3
Os arquivos s
ao considerados como vari
aveis permanentes, cujos valores podem ser lidos ou
atualizados em momentos futuros, depois que o programa que os criou tenha terminada a sua
desnecessario comentar a import
execucao. E
ancia que estas variaveis tem sobre a computacao
e a necessidade de suas existencias. No entanto, e necessaria uma forma de comunicacao do
usu
ario com os arquivos. J
a foi vista uma forma, atraves do comando do. A outra, que ser
a
vista a seguir, e atraves de descritores.
Para obter um descritor (do tipo Handle) de um arquivo e necessaria a operacao de abertura
deste arquivo para que futuras operacoes de leitura e/ou escrita possam acontecer. Alem disso, e
necessaria uma operacao de fechamento deste arquivo, ap
os suas operacoes terem sido realizadas,
para que os dados que ele deve conter, n
ao sejam perdidos quando o programa de usu
ario
terminar sua execucao. Estas operacoes sao descritas em Haskell, da seguinte forma:
data IOMode = ReadMode | WriteMode | AppendMode | ReadWriteMode
openFile :: FilePath -> IOMode -> IO Handle
hClose :: Handle -> IO ()
Por convencao, todas as funcoes (normalmente chamadas de comandos) usadas para tratar
com descritores de arquivos sao iniciadas com a letra h. Por exemplo, as funcoes
hPutChar :: Handle -> Char -> IO ()
hPutStr :: Handle -> String -> IO ()
hPutStrLn :: Handle -> String -> IO ()
hPrint :: Show a => Handle -> a -> IO ()
sao utilizadas para escrever alguma coisa em um arquivo. Ja os comandos
hGetChar :: Handle -> IO ()
hGetLine :: Handle -> IO ()
sao utilizados nas operacoes de leituras em arquivos. As funcoes hPutStrLn e hPrint incluem
um caractere \n para a mudanca de linha ao nal da string.
Haskell tambem permite que todo o conte
udo de um arquivo seja retornado como uma u
nica
string, atraves da funcao
hGetContents :: Handle -> String
149
No entanto, h
a que se fazer uma observacao. Apesar de parecer que hGetContents retorna
todo o conte
udo de um arquivo de uma u
nica vez, n
ao e realmente isto o que acontece. Na
realidade, a funcao retorna uma lista de caracteres, como esperado, mas de forma lazy, onde os
elementos sao lidos sob demanda.
6.3.1
Lembremos que um arquivo tambem pode ser escrito sem o uso de descritores. Por exemplo,
pode-se escrever em um arquivo usando o comando
type FilePath = String
writeFile :: FilePath -> String -> IO ()
Tambem podemos acrescentar uma string ao nal de um arquivo com o comando
appendFile :: FilePath -> String -> IO ()
Ent
ao, qual a necessidade de se utilizar descritores? A resposta e imediata: eciencia. Vamos
analisar.
Toda vez que o comando writeFile ou appendFile e executado, deve acontecer tambem
uma seq
uencia de acoes, ou seja, o arquivo deve ser inicialmente aberto, a string deve ser escrita
e, nalmente, o arquivo deve ser fechado. Se muitas operacoes de escrita forem necessarias,
entao serao necessarias muitas destas seq
uencias. Ao se utilizar descritores, necessita-se apenas
de uma operacao de abertura no incio e outra de fechamento no nal.
6.3.2
Canais
Os descritores tambem podem ser associados a canais, que sao portas de comunicacao n
ao
associadas diretamente a um arquivo. Os canais mais comuns s
ao: a entrada padr
ao (stdin),
a area de sada padr
ao (stdout) e a area de erro padr
ao (stderr). As operacoes de IO para
caracteres e strings em canais incluem as mesmas listadas anteriormente para a manipulacao de
arquivos. Na realidade, as funcoes getChar e putChar sao denidas como:
getChar = hGetChar stdin
putChar = hputChar stdout
Ate mesmo hGetContents pode ser usada com canais. Neste caso, o m de um canal e
sinalizado com um cartactere de m de canal que, na maioria dos sistemas, e Ctrl-d.
6.4
Gerenciamento de excec
oes
Vamos agora nos reportar a erros que podem acontecer durante as operacoes de IO. Por exemplo,
pode-se tentar abrir um aquivo que ainda n
ao existe, ou pode-se tentar ler um caractere de um
arquivo que j
a atingiu seu nal. Certamente, n
ao se deve querer que o programa p
are por
estes motivos. O que normalmente se espera, e que o erro seja reportado como uma condicao
an
omala, mas que possa ser corrigida, sem a necessidade de que o programa seja abortado. Para
fazer este gerenciamento de excecoes, em Haskell, sao necessarios apenas alguns comandos de
IO.
As excecoes tem o tipo IOError. Entre as operacoes permitidas sobre este tipo, esta uma
colecao de predicados que podem ser usados para testar tipos particulares de excecoes. Por
exemplo,
150
6.5
Resumo
Este foi o Captulo nal deste trabalho, dedicado a` semantica de acoes, adotadas em Haskell,
para tratar operacoes de entrada e sada, representando a comunicacao que o programa deve
ter com perifericos e com os arquivos. Na realidade, ela e implementada em Haskell atraves de
M
onadas, uma teoria matematica bastante complexa e que, por este motivo, esta fora do escopo
deste estudo.
O objetivo do Captulo foi mostrar as formas como Haskell trata as entradas e as sadas de
dados, ou seja, que facilidades a linguagem Haskell oferece para a comunicacao com o mundo
151
externo ao programa. Isto inclui a leitura e escrita de dados em arquivos, bem como a criacao
e o fechamento de arquivos, armazenando dados para serem utilizados futuramente.
Este estudo foi baseado nos livros de Simon Thompson [35], de Richard Bird [4] e de Paul
Hudak [14]. Este ainda e considerado um tema novo pelos pesquisadores das linguagens funcionais e acreditamos ser este o motivo que o ele e ainda muito pouco tratado na literatura. Outra
possibilidade da ausencia de publicacoes nesta area pode ser o grau de diculdade imposto no
estudo dos M
onadas, que e considerado muito alto pela grande maioria dos pesquisadores da
area.
Com este Captulo, esperamos ter cumprido nosso objetivo inicial que foi o de proporcionar
aos estudantes iniciantes das linguagens funcionais, um pouco da fundamentacao destas linguagens e de suas aplicacoes, notadamente em programacao funcional usando Haskell, a linguagem
funcional mais em uso, no momento.
152
Bibliografia
[1] AHO, Alfred V; SETHI, Ravi et ULLMAN, Jerey D. Compilers, Principles, Techniques,
and Tools. 2nd. Edition. Addison-Wesley Publishing Company; 1988.
[2] ANDRADE, Carlos Anreazza Rego. AspectH: Uma Extens
ao Orientada a Aspectos de
Haskell. Dissertacao de Mestrado. Centro de Inform
atica. UFPE. Recife, Fevereiro 2005.
[3] BARENDREGT, H. P. em The Lambda Calculus: Its Syntax and Semantics. (Revised
Edn.). North Holland, 1984.
[4] BIRD, Richard. Introduction to Functional Programming Using Haskell. 2nd. Edition.
Prentice Hall Series in Computer Science - Series Editors C. A. Hoare and Richard Bird.
1998.
[5] BRAINERD, W. S. et LANDWEBER, L. H. Theory of Computation. John Wiley & Sons,
1974.
[6] CURRY, H. B. et FEYS, R. and CRAIG, W. Combinatory Logic. Volume I; North Holland,
1958.
[7] DAVIE, Antony J. T. An Introduction to Functional Programming Systems Using Haskell.
Cambridge Computer Science Texts. Cambridge University Press. 1999.
[8] DE SOUZA, Francisco Vieira. Gerenciamento de Mem
oria em CMC. Dissertacao de
Mestrado. CIn-UFPE. Marco de 1994.
[9] DE SOUZA, Francisco Vieira. Teoria das Categorias: A Linguagem da Computac
ao.
Exame de Qualicacao. Centro de Inform
atica. UFPE. 1996.
[10] DE SOUZA, Francisco Vieira et LINS, Rafael Dueire. Aspectos do Comportamento Espacotemporal de Programas Funcionais em Uni e Multiprocessadores. X Simp
osio Brasileiro de
Arquitetura de Computadores e Processamento de Alto Desempenho. B
uzios-RJ. Setembro. 1998.
[11] DE SOUZA, Francisco Vieira et LINS, Rafael Dueire. Analysing Space Behaviour of
Functional Programs. Conferencia Latino-americana de Programacao Funcional. RecifePe. Marco. 1999.
[12] DE SOUZA, Francisco Vieira. Aspectos de Eciencia em Algoritmos para o Gerenciamento
Autom
atico Din
amico de Mem
oria. Tese de Doutorado. Centro de Inform
atica-UFPE.
Recife. Novembro. 2000.
[13] HINDLAY, J. Roger. Basic Simple Type Theory. Cambridge Tracts in Theorical Computer
Science, 42. Cambridge University Press. 1997.
[14] HUDAK, Paul. The Haskell School of Expression: Learning Funciotonal Programming
Through Multimedia. Cambridge University Press, 2000.
153
[15] HUGHES, John. Why Functional Programming Matters. In Turner D. T. Ed. Research
Topics in Funcitonal Programming. Addison-Wesley, 1990.
[16] JOHNSSON, T. Ecient Computation of Lazy Evaluation. Proc. SIGPLAN84. Symposium on Compiler Construction ACM. Montreal, 1984.
[17] JOHNSSON, T. Lambda Lifting: Transforming Programs to Recursive Equations. Aspen
as
Workshop on Implementation of Functional Languages. G
oteborg, 1985.
[18] JOHNSSON, T. Target Code Generation from G-Machine Code. Proc. Workshop on
Graph Reduction, Santa Fe Lecture Notes on Computer science, Vol: 279 pp. 119-159.
Spring-Verlag, 1986.
[19] JOHNSSON, T. Compiling Lazy Functional Languages. Ph.D. Thesis. Chalmers University
of Technology, 1987.
[20] LANDIN, P. J. The Mechanical Evaluation of Expressions. Computer Journal, Vol. 6, 4.
1964.
[21] LINS, Rafael Dueire et LIRA, Bruno O. CMC: A Novel Way of Compiling Functional
Languages. J. Programming Languages 1:19-40; Chapmann & Hall. 1993.
[22] LINS, Rafael Dueire. O -C
alculo, Computabilidade & Lingugens de Programac
ao. Notas
de Curso. Recife-Pe, Nov. 1993.
[23] LINS, Rafael Dueire et all. Research Interests in Functional Programming. I Workshop
on Formal Methods. UFRGS. Outubro, 1998.
[24] MACLENNAN, Bruce J. Functional Programming Practice. Addison-Wesley Publishing
Company, Inc. 1990.
[25] MEIRA, Slvio Romero de Lemos. Introduc
ao `
a programac
ao Funcional. VI Escola de
Computacao. Campinas, 1988.
[26] OKASAKI, Chris. Purely Functional Data Structures. Cambridge University Press. 2003.
[27] PAULSON, Laurence C. ML for the Working Programmer. Cambridge University Press,
1991.
[28] PEMMARAJU, Sriram et SKIENA, Steven. Computational Discrete Mathematics: Combinatorics and Graph Theory with Mathematica. Cambridge University Press. 2003.
[29] PEYTON JONES, S. L. The Implementation of Functional Programming Languages. C.
A. R. Hoare Series Editor. Prentice/Hall International. 1987.
[30] RABHI, Fethi et LAPALME, Guy. Algorithms: A Functional Programming Approach.
2nd. edition. Addison-Wesley. 1999.
[31] SCHMIDT, D. A. Denotational Semantics. Allyn and Bacon, Inc. Massachusetts, 1986.
[32] SCOTT, D. Data Types as Lattices. SIAM Journal of Computing. Vol. 5,3. 1976.
[33] SKIENA, Steven S. et REVILLA, Miguel A. Programming Challenges: The Programming
Context Training Manual. Texts in Computer Science. Springer Science+Business Media,
Inc. 2003.
[34] STOY, J. E. Denotational Semantics: The Scott-Strachey Approach to Programming
Language Theory. MIT Press, 1977.
154
[35] THOMPSON, Simon. Haskell: The Craft of Functional Programming. 2nd. Edition.
Addison Wesley. 1999.
[36] TURNER, David A. A New Implementation Technique for Applicative Languages. Software Practice and Experience. Vol. 9. 1979.
[37] WADLER, Philip. Why no ones uses functional languages. Functional Programming.
ACM SIGPLAN. 2004.
[38] WELCH, Peter H. The -Calculus. Course notes, The University of Kent at Canterbury,
1982.
155