Docsity
Docsity

Prepare-se para as provas
Prepare-se para as provas

Estude fácil! Tem muito documento disponível na Docsity


Ganhe pontos para baixar
Ganhe pontos para baixar

Ganhe pontos ajudando outros esrudantes ou compre um plano Premium


Guias e Dicas
Guias e Dicas


Programação Funcional em Haskell, Notas de estudo de Informática

Programação Funcional em Haskell

Tipologia: Notas de estudo

Antes de 2010

Compartilhado em 06/05/2009

victor-luiz-piza-soares-1
victor-luiz-piza-soares-1 🇧🇷

5 documentos

1 / 163

Toggle sidebar

Esta página não é visível na pré-visualização

Não perca as partes importantes!

bg1
PROGRAMAC¸˜
AO FUNCIONAL
USANDO HASKELL
pf3
pf4
pf5
pf8
pf9
pfa
pfd
pfe
pff
pf12
pf13
pf14
pf15
pf16
pf17
pf18
pf19
pf1a
pf1b
pf1c
pf1d
pf1e
pf1f
pf20
pf21
pf22
pf23
pf24
pf25
pf26
pf27
pf28
pf29
pf2a
pf2b
pf2c
pf2d
pf2e
pf2f
pf30
pf31
pf32
pf33
pf34
pf35
pf36
pf37
pf38
pf39
pf3a
pf3b
pf3c
pf3d
pf3e
pf3f
pf40
pf41
pf42
pf43
pf44
pf45
pf46
pf47
pf48
pf49
pf4a
pf4b
pf4c
pf4d
pf4e
pf4f
pf50
pf51
pf52
pf53
pf54
pf55
pf56
pf57
pf58
pf59
pf5a
pf5b
pf5c
pf5d
pf5e
pf5f
pf60
pf61
pf62
pf63
pf64

Pré-visualização parcial do texto

Baixe Programação Funcional em Haskell e outras Notas de estudo em PDF para Informática, somente na Docsity!

PROGRAMAC¸ ˜AO FUNCIONAL

USANDO HASKELL

Programa¸c˜ao Funcional

Usando Haskell

Francisco Vieira de Souza

  • Licenciado em Matem´atica (1978) e Engenheiro Civil (1982) pela Universidade Federal do Piau´ı, Mestre (1994) e Doutor (2000) em Ciˆencia da Computa¸c˜ao pela Universidade Federal de Pernambuco.
  • Professor do Departamento de Matem´atica (1986-1988), fundador (1987) e professor (desde
    1. do Departamento de Inform´atica e Estat´ıstica da Universidade Federal do Piau´ı.

UFPI/CCN/DIE

Teresina-Pi Agosto de 2005

Apresenta¸c˜ao

Esta Apostila representa a compila¸c˜ao de v´arios t´opicos, desenvolvidos por pesquisadores de renome, no campo da programa¸c˜ao funcional. Ela tem como objetivo servir de guia aos profis- sionais de Inform´atica que desejam ter um conhecimento inicial sobre o paradigma funcional de forma geral e, em particular, sobre programa¸c˜ao usando Haskell. Ultimamente, Haskell tem se tornado a linguagem funcional padr˜ao do discurso, j´a existindo v´arios interpretadores e compi- ladores para ela, al´em de v´arias ferramentas de an´alise de programas nela codificados (profiles).

Para atingir este objetivo, acreditamos que o estudo deva ser acompanhado de algum co- nhecimento, mesmo que m´ınimo, sobre a fundamenta¸c˜ao destas linguagens e da forma como elas s˜ao implementadas. Este conhecimento proporciona ao leitor uma vis˜ao das principais ca- racter´ısticas e propriedades destas linguagens. Em particular, ´e importante entender porque as t´ecnicas utilizadas na compila¸c˜ao das linguagens imperativas n˜ao se mostraram adequadas na compila¸c˜ao de linguagens funcionais.

Em 1978, John Backus advogou o paradigma funcional como o que oferecia a melhor solu¸c˜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 implementa¸c˜ao eficiente de linguagens funcionais. Esse trabalho provocou uma corrida em dire¸c˜ao `a pesquisa nesta ´area, gerando uma variedade de t´ecnicas de implementa¸c˜ao destas linguagens.

Dentre estas t´ecnicas, uma que tem sido adotada, com resultados promissores, ´e a utiliza¸c˜ao do λ-c´alculo como linguagem intermedi´aria entre a linguagem de alto n´ıvel e a linguagem de m´aquina. Os programas codificados em alguma linguagem funcional de alto n´ıvel 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 compila¸c˜ao de linguagens imperativas. Esta metodologia tem dado certo, uma vez que j´a se conhecem t´ecnicas eficientes de tradu¸c˜ao de programas em λ-c´alculo para programas execut´aveis, faltando apenas uma tradu¸c˜ao eficiente de programas codificados em uma linguagem funcional de alto n´ıvel para programas em λ-c´alculo.

Esta Apostila tem in´ıcio em seu primeiro Cap´ıtulo tratando das caracter´ısticas das lingua- gens funcionais, destacando suas vantagens em rela¸c˜ao as linguagens de outros paradigmas que utilizam atribui¸c˜oes destrutivas. Em seguida, ´e feita uma introdu¸c˜ao ao λ-c´alculo. Apesar do car´ater introdut´orio, achamos ser suficiente para quem quer dar os primeiros passos em dire¸c˜aoa aprendizagem desta t´ecnica. Os Cap´ıtulos subseq¨uentes se referem todos `a Programa¸c˜ao Fun- cional usando Haskell.

Por ser uma primeira tentativa, a Apostila cont´em erros e sua apresenta¸c˜ao did´atico-peda- g´ogica deve ser revista. Neste sentido, agradecemos cr´ıticas construtivas que ser˜ao objeto de an´alise e reflex˜ao e, por isto mesmo, muito bem-vindas.

Teresina-Pi, agosto de 2005.

Francisco Vieira de Souza

iv

Conte´udo

  • 1 Programa¸c˜ao Funcional Introdu¸c˜ao viii
    • 1.1 Introdu¸c˜ao
    • 1.2 Hist´orico
    • 1.3 Programa¸c˜ao com express˜oes
    • 1.4 Independˆencia da ordem de avalia¸c˜ao
    • 1.5 Transparˆencia referencial
    • 1.6 Interfaces manifestas
    • 1.7 Fun¸c˜oes e express˜oes aplicativas
    • 1.8 Defini¸c˜ao de fun¸c˜oes
    • 1.9 Resumo
  • 2 λ-c´alculo
    • 2.1 Introdu¸c˜ao
    • 2.2 λ-express˜oes
    • 2.3 A sintaxe do λ-c´alculo
      • 2.3.1 Aplica¸c˜ao de fun¸c˜ao e currifica¸c˜ao
    • 2.4 Fun¸c˜oes e constantes pr´e-definidas
    • 2.5 λ-abstra¸c˜oes
    • 2.6 A semˆantica operacional do λ-c´alculo
      • 2.6.1 Formaliza¸c˜ao das ocorrˆencias livres ou ligadas
    • 2.7 Combinadores
    • 2.8 Regras de convers˜oes entre λ-express˜oes
      • 2.8.1 α-convers˜ao
      • 2.8.2 η-convers˜ao
      • 2.8.3 β-convers˜ao
      • 2.8.4 Nomea¸c˜ao
      • 2.8.5 Captura
    • 2.9 Convers˜ao, redu¸c˜ao e abstra¸c˜ao
    • 2.10 Provando a conversibilidade
    • 2.11 Uma nova nota¸c˜ao
    • 2.12 Ordem de redu¸c˜ao
    • 2.13 Fun¸c˜oes recursivas
    • 2.14 Resumo
  • 3 Programa¸c˜ao funcional em Haskell
    • 3.1 Introdu¸c˜ao
    • 3.2 Primeiros passos
      • 3.2.1 O interpretador Hugs
      • 3.2.2 Identificadores em Haskell
    • 3.3 Fun¸c˜oes em Haskell
      • 3.3.1 Construindo fun¸c˜oes
      • 3.3.2 Avalia¸c˜ao de fun¸c˜oes em Haskell
      • 3.3.3 Casamento de padr˜oes (patterns matching)
    • 3.4 Tipos de dados em Haskell
      • 3.4.1 Os tipos primitivos da linguagem
      • 3.4.2 Programando com n´umeros e strings
      • 3.4.3 Os tipos de dados estruturados de Haskell
      • 3.4.4 Escopo
      • 3.4.5 C´alculos:
      • 3.4.6 Projeto de programas
      • 3.4.7 Provas de programas
    • 3.5 Resumo
  • 4 O tipo Lista
    • 4.1 Fun¸c˜oes sobre listas
      • 4.1.1 O construtor de listas : (cons)
      • 4.1.2 Construindo fun¸c˜oes sobre listas
    • 4.2 Pattern matching revisado
    • 4.3 Compreens˜oes e express˜oes ZF (Zermelo-Fraenkel)
    • 4.4 Fun¸c˜oes de alta ordem
      • 4.4.1 A fun¸c˜ao map
      • 4.4.2 Fun¸c˜oes anˆonimas
    • 4.5 Polimorfismo
      • 4.5.1 Tipos vari´aveis
      • 4.5.2 O tipo mais geral
    • 4.6 Indu¸c˜ao estrutural
    • 4.7 Composi¸c˜ao de fun¸c˜oes
      • 4.7.1 Composi¸c˜ao avan¸cada
      • 4.7.2 Esquema de provas usando composi¸c˜ao
    • 4.8 Aplica¸c˜ao parcial
      • 4.8.1 Se¸c˜ao de operadores
      • 4.8.2 Currifica¸c˜ao
    • 4.9 Melhorando o desempenho de uma implementa¸c˜ao
      • 4.9.1 O desempenho da fun¸c˜ao reverse
      • 4.9.2 O desempenho do quicsort
    • 4.10 Resumo
  • 5 Tipos de dados complexos
    • 5.1 Introdu¸c˜ao
    • 5.2 Classes de tipos
      • 5.2.1 Fundamenta¸c˜ao das classes
      • 5.2.2 Fun¸c˜oes que usam igualdade
      • 5.2.3 Assinaturas e instˆancias
      • 5.2.4 Classes derivadas
      • 5.2.5 As classes pr´e-definidas em Haskell
    • 5.3 Tipos alg´ebricos
      • 5.3.1 Como se define um tipo alg´ebrico?
      • 5.3.2 A forma geral
      • 5.3.3 Derivando instˆancias de classes
      • 5.3.4 Tipos recursivos
      • 5.3.5 Recurs˜ao m´utua
      • 5.3.6 Tipos alg´ebricos polim´orficos
    • 5.4 Tratamento de erros
      • 5.4.1 Valores fict´ıcios
      • 5.4.2 Tipos de erros
    • 5.5 Provas sobre tipos alg´ebricos
    • 5.6 M´odulos em Haskell
      • 5.6.1 Cabe¸calho em Haskell
      • 5.6.2 Importa¸c˜ao de m´odulos
      • 5.6.3 O m´odulo main
      • 5.6.4 Controles de exporta¸c˜ao
      • 5.6.5 Controles de importa¸c˜ao
    • 5.7 Tipos abstratos de dados
      • 5.7.1 O tipo abstrato Pilha
      • 5.7.2 O tipo abstrato de dado Fila
      • 5.7.3 O tipo abstrato Set
      • 5.7.4 O tipo abstrato Tabela
    • 5.8 Lazy evaluation
      • 5.8.1 Express˜oes ZF (revisadas)
      • 5.8.2 Dados sob demanda
      • 5.8.3 Listas infinitas
    • 5.9 Resumo
  • 6 Programa¸c˜ao com a¸c˜oes em Haskell
    • 6.1 Introdu¸c˜ao
    • 6.2 Entrada e Sa´ıda em Haskell
      • 6.2.1 Opera¸c˜oes de entrada
      • 6.2.2 Opera¸c˜oes de sa´ıda
      • 6.2.3 A nota¸c˜ao do
    • 6.3 Arquivos, canais e descritores
      • 6.3.1 A necessidade dos descritores
      • 6.3.2 Canais
    • 6.4 Gerenciamento de exce¸c˜oes
    • 6.5 Resumo
  • Referˆencias Bibliogr´aficas

e sim com as bibliotecas associadas, usadas na constru¸c˜ao de gr´aficos, banco de dados, interfaceamento, telefonia e servidores. Apesar de ainda n˜ao existirem muitas bibliotecas gr´aficas para as linguagens funcionais, muito esfor¸co tem sido feito nesta dire¸c˜ao, nos ultimos tempos.´ Haskell tem Fudgets, Gadgets, Haggis e Hugs Tk. SML/NJ tem duas: eXene e SML Tk. Haskell e ML tˆem ambas um poderoso sistema de m´odulos que tornam suas bibliotecas f´aceis de serem constru´ıdas.

  • Portabilidade. Inegavelmente C e C++ tˆem sido preferidas em muitos projetos. No entanto, muito desta preferˆencia n˜ao se deve ao fato de C gerar um c´odigo mais r´apido que o c´odigo gerado pelas linguagens funcionais, apesar de, normalmente, se verificar esta diferen¸ca de desempenho. Na realidade, esta preferˆencia se deve mais `a portabilidade ineg´avel de C. Sabe-se que os pesquisadores em Lucent teriam preferido construir a lin- guagem PRL para Banco de Dados usando SML, mas escolheram C++, porque SML n˜ao estava dispon´ıvel no mainframe Amdahl, onde deveria ser utilizada. Por outro lado, as t´ecnicas de implementa¸c˜ao de linguagens utilizando m´aquinas abstratas tˆem se tornado muito atrativas para linguagens funcionais [21] e tamb´em para Java. Isto se deve muito ao fato de que escrever a m´aquina abstrata em C a torna muito mais f´acil de ser portada para uma grande variedade de arquiteturas.
  • Disponibilidade. Alguns compiladores s˜ao muito dif´ıceis de serem instalados. Por exem- plo, GHC (Glasgow Haskell Compiler) era considerado uma aventura por alguns usu´arios que tentavam instal´a-lo. Ainda existem poucas linguagens funcionais comerciais e isto torna dif´ıcil um c´odigo est´avel e um suporte confi´avel. Al´em do mais, as linguagens funci- onais est˜ao em permanente desenvolvimento e, portanto, est˜ao sempre em transforma¸c˜oes.
  • Empacotamento. Muitas linguagens funcionais seguem a tradi¸c˜ao de LISP, de sempre realizar suas implementa¸c˜oes atrav´es do loop read-eval-print. Apesar da conveniˆencia, ´e essencial desenvolver habilidades para prover alguma forma de convers˜ao de programas funcionais em programas de aplica¸c˜ao standalone. Muitos sistemas j´a oferecem isto, no entanto, incorporam o pacote de runtime completo `a biblioteca e isto implica na exigˆencia de muita mem´oria.
  • Ferramentas. Uma linguagem para ser utiliz´avel necessita de ferramentas para depura¸c˜ao e profiler. Estas ferramentas s˜ao f´aceis de serem constru´ıdas para linguagens estritas, no entanto, s˜ao muito dif´ıceis de serem constru´ıdas para linguagens lazy, onde a ordem de avalia¸c˜ao n˜ao ´e conhecida a priori. Verifica-se uma exce¸c˜ao em Haskell, onde muitas ferramentas de profiler j´a est˜ao dispon´ıveis.
  • Treinamento. Para programadores imperativos ´e muito dif´ıcil programar funcionalmente. Uma solu¸c˜ao imperativa ´e mais f´acil de ser entendida e de ser encontrada em livros ou artigos. Uma solu¸c˜ao funcional demora mais tempo para ser criada, apesar de muito mais elegante. Por este motivo, muitas linguagens funcionais atuais provˆeem um escape para o estilo imperativo. Isto pode ser verificado em ML que n˜ao ´e considerada uma linguagem funcional pura, porque permite atribui¸c˜oes destrutivas. Haskell ´e uma linguagem funcional pura, mas consegue imitar as atribui¸c˜oes das linguagens imperativas utilizando uma teoria funcional complexa que ´e a semˆantica de a¸c˜oes, implementadas atrav´es de mˆonadas^2.
  • Popularidade. Se um gerente escolher uma linguagem funcional para ser utilizada em um projeto e este falhar, provavelmente ele ser´a crucificado. No entanto, se ele escolher C ou C++ e n˜ao tiver sucesso, tem a seu favor o argumento de que o sucesso de C++ j´a foi verificado em in´umeros casos e em v´arios locais. (^2) A semˆantica de a¸c˜oes ´e um tema tratado no Cap´ıtulo 6. Os mˆonadas n˜ao s˜ao aqui tratados, uma vez que seu estudo requer, como pr´e-requsito, o conhecimento aprofundado sobre implementa¸c˜ao de linguagens funcionais.

2

  • Desempenho. H´a uma d´ecada atr´as, os desempenhos dos programas funcionais eram bem menores que os dos programas imperativos, mas isto tem mudado muito ultimamente. Hoje, os desempenhos de muitos programas funcionais s˜ao melhores ou pelo menos est˜ao em “p´es de igualdade” com seus correspondentes em C. Isto depende da aplica¸c˜ao. Java tem uma boa aceita¸c˜ao e, no entanto, seu desempenho ´e muito inferior a C, na grande maioria das aplica¸c˜oes. Na realidade, existem linguagens com alto desempenho que n˜ao s˜ao muito utilizadas e existem linguagens com desempenho mediano com alta taxa de utiliza¸c˜ao. Desempenho ´e um fator importante, mas n˜ao tem se caracterizado como um fator decisivo na escolha de uma linguagem.

Resumidamente, existem muitos fatores que desencorajam a escolha de linguagens funcio- nais como forma de se codificar programas. Para ser fortemente utilizada, uma linguagem deve suportar trabalho interativo, possuir bibliotecas extensas, ser altamente port´avel, ter uma im- plementa¸c˜ao est´avel e f´acil de ser instalada, ter depuradores e profilers, ser acompanhada de cursos de treinamentos e j´a ter sido utilizada, com sucesso, em uma boa quantidade de projetos.

Para Wadler [37] todos estes requisitos j´a s˜ao perfeitamente atendidos por algumas linguagens funcionais, por exemplo Haskell. Para ele o que ainda existe ´e um preconceito injustific´avel por parte de alguns programadores de outros paradigmas de programa¸c˜ao. No entanto, para a felicidade e consolo dos admiradores das linguagens funcionais, esta cultura tem se modificado, e de forma r´apida, ao longo dos ´ultimos anos.

A nosso ver, o que existe mesmo ´e a falta de informa¸c˜ao e conhecimento do que realmente ´e programa¸c˜ao funcional e quais as suas vantagens e desvantagens em rela¸c˜ao `a programa¸c˜ao em outros paradigmas. Esta Apostila tenta prover subs´ıdios para que seus usu´arios possam iniciar um processo de discuss˜ao sobre o tema. Para desencadear este processo, ´e necess´ario come¸car pelo entendimento da rela¸c˜ao entre a programa¸c˜ao funcional e a programa¸c˜ao estruturada.

Princ´ıpios da programa¸c˜ao estruturada

Hoare enumerou seis princ´ıpios fundamentais da estrutura¸c˜ao de programas [24]:

  1. Transparˆencia de significado. Este princ´ıpio afirma que o significado de uma express˜ao, como um todo, pode ser entendido em termos dos significados de suas sub-express˜oes. Assim, o significado da express˜ao E + F depende simplesmente dos significados das sub- express˜oes E e F, independente das complica¸c˜oes de cada uma delas.
  2. Transparˆencia de prop´ositos. Textualmente, Hoare diz: “o prop´osito de cada parte consiste unicamente em sua contribui¸c˜ao para o prop´osito do todo”. Assim, em E + F, o ´unico prop´osito de E ´e computar o n´umero que ser´a o operando esquerdo do operador “+”. Isto significa que seu prop´osito n˜ao inclui qualquer efeito colateral.
  3. Independˆencia das partes. Este princ´ıpio apregoa que os significados 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 princ´ıpio se refere ao fato de que as express˜oes aritm´eticas s˜ao constru´ıdas pela aplica¸c˜ao recursiva de regras uniformes. Isto significa que se n´os sabemos que E e F s˜ao express˜oes, ent˜ao sabemos que E + F tamb´em ´e uma express˜ao.
  5. Interfaces pequenas. As express˜oes aritm´eticas tˆem interfaces pequenas, porque cada opera¸c˜ao aritm´etica tem apenas uma sa´ıda e apenas uma ou duas entradas. Al´em disso,

3

  • ´e uma linguagem onde os programas nela codificados consistem inteiramente de fun¸c˜oes,
  • ´e uma linguagem que n˜ao tem side effects,
  • ´e uma linguagem em que a ordem de execu¸c˜ao ´e irrelevante, ou seja, n˜ao precisa analisar o fluxo de controle,
  • ´e uma linguagem onde pode-se substituir, a qualquer tempo, vari´aveis por seus valores (transparˆencia referencial) ou
  • ´e uma linguagem cujos programas nela escritos s˜ao mais trat´aveis matematicamente.

Estas respostas tamb´em n˜ao s˜ao conclusivas, da mesma forma que as respostas dadas `a quest˜ao sobre programa¸c˜ao estruturada. E qual seria uma resposta afirmativa e definitiva? Usando a resposta anterior como base, pode-se dizer que ”as linguagens funcionais s˜ao altamente modulariz´aveis.”.

Esta ´e uma resposta afirmativa e que necessita apenas de mais um complemento para que a quest˜ao fique respondida em seu todo. Este complemento se refere `as caracter´ısticas que as linguagens funcionais apresentam e que proporcionam esta melhoria na modularidade dos sistemas. Estas caracter´ısticas podem ser sumarizadas na seguinte observa¸c˜ao: “a programa¸c˜ao funcional melhora a modularidade, provendo m´odulos menores, mais simples e mais gerais, atrav´es das fun¸c˜oes de alta ordem e lazy evaluation.

Estas duas caracter´ısticas, verificadas apenas nas linguagens funcionais, ´e que s˜ao res- pons´aveis pela grande modularidade por elas proporcionada. Dessa forma, as linguagens funci- onais s˜ao altamente estruturadas e se candidatam, com grande chance de ˆexito, como solu¸c˜oes para a t˜ao propalada “crise do software” dos anos 80.

Para continuar esta viagem pelo mundo da programa¸c˜ao funcional, tentando entender sua fundamenta¸c˜ao te´orica ´e necess´ario conhecer tamb´em como as linguagens s˜ao implementadas. Inicialmente vamos nos referir `a implementa¸c˜ao de linguagens tradicionais e depois particulari- zaremos para o caso das funcionais.

O processo de compila¸c˜ao de linguagens

Programar ´e modelar problemas do mundo real ou imagin´ario em um computador, usando algum paradigma de programa¸c˜ao. Desta forma, os problemas s˜ao modelados em um n´ıvel de abstra¸c˜ao bem mais alto atrav´es de especifica¸c˜oes formais, feitas utilizando uma linguagem de especifica¸c˜ao formal. Existem v´arias linguagens de especifica¸c˜ao formal: Lotos, Z, VDM, Redes de Petri, entre outras. A escolha de uma delas est´a diretamente ligada ao tipo da aplica¸c˜ao e `a experiˆencia do programador. Por exemplo, para especificar dispositivos de hardware ´e mais natural se usar Redes de Petri, onde pode-se verificar a necessidade de sincroniza¸c˜ao e podem ser feitas simula¸c˜oes para a an´alise de desempenho ou detectar a existˆencia, ou n˜ao, de inconsistˆencias.

Muitas ferramentas gr´aficas j´a existem e s˜ao utilizadas na an´alise de desempenho de dispo- sitivos especificados formalmente e podem ser feitos prot´otipos r´apidos para se verificar a ade- quabilidade das especifica¸c˜oes `as exigˆencias 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 especifica¸c˜ao formal representa um prova da corretude do programa e que ele faz exata- mente o que foi projetado para fazer. Isto pode ser comparado com a garantia que um usu´ario tem quando compra um eletrodom´estico em uma loja. Esta garantia n˜ao pode ser dada pelos testes, uma vez que eles s´o podem verificar a presen¸ca de erros, nunca a ausˆencia deles. Os testes representam uma ferramenta importante na ausˆencia de uma prova da corretude de um

5

programa, mas n˜ao representam uma prova. O uso de testes requer que eles sejam bem proje- tados e de forma objetiva, para que tenham a sua existˆencia justificada. Com a especifica¸c˜ao pronta, ela deve ser implementada em uma linguagem de programa¸c˜ao.

Figura 1: Relacionamento entre Computador e Linguagens.

A linguagem de programa¸c˜ao a ser escolhida depende da aplica¸c˜ao. Em muitos casos, este processo ´e t˜ao somente uma tradu¸c˜ao, dependendo da experiˆencia do programador com a lingua- gem de programa¸c˜ao escolhida. No caso das linguagens funcionais, este processo ´e muito natural, uma vez que as especifica¸c˜oes formais s˜ao apenas defini¸c˜oes impl´ıcitas de fun¸c˜oes, restanto ape- nas a tradu¸c˜ao destas defini¸c˜oes impl´ıcitas para defini¸c˜oes expl´ıcitas na linguagem funcional escolhida^3. Resta agora a tradu¸c˜ao destes programas em linguagens de alto n´ıvel para progra- mas execut´aveis em linguagem de m´aquina, sendo este o papel do compilador. Este processo est´a mostrado na Figura 1.

A compila¸c˜ao de programas codificados em linguagens imperativas, normalmente, ´e feita em duas etapas [1]. Na primeira delas, ´e feita uma tradu¸c˜ao do programa escrito em linguagem de alto n´ıvel para um programa codificado em linguagem intermedi´aria, chamada de linguagem de montagem (Assembly). Na etapa seguinte, ´e feita a tradu¸c˜ao do programa em linguagem de montagem para o programa execut´avel, em linguagem de m´aquina. Este processo est´a mostrado na Figura 2.

Figura 2: Processo de compila¸c˜ao das linguagens imperativas. (^3) Estas formas de defini¸c˜ao de fun¸c˜oes s˜ao mostradas no Cap´ıtulo 1 desta Apostila.

M´aquinas abstratas

Uma t´ecnica usada com sucesso na implementa¸c˜ao de linguagens funcionais consiste na imple- menta¸c˜ao do λ-c´alculo usando a redu¸c˜ao das λ-express˜oes para λ-express˜oes mais simples, at´e atingir uma forma normal, se ela existir. O resultado desta t´ecnica foi um sistema que ficou 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 m´aquina SECD usa a pilha S para a avalia¸c˜ao das λ-express˜oes Codificadas, utilizando o ambiente E.

Uma otimiza¸c˜ao importante na m´aquina SECD foi transferir alguma parcela do tempo de execu¸c˜ao para o tempo de compila¸c˜ao. No caso, isto foi feito transformando as express˜oes com nota¸c˜ao infixa para a nota¸c˜ao polonesa reversa que ´e adequada para ser executada em pilha, melhorando o ambiente de execu¸c˜ao. Para diferenciar da m´aquina SECD, esta m´aquina foi chamada de SECD2.

Em 1979, David Turner [36] desenvolveu um processo de avalia¸c˜ao de express˜oes em SASL (uma linguagem funcional) usando o que ficou conhecida como a m´aquina de combinadores. Ele utilizou os combinadores S, K e I do λ-c´alculo^5 para representar express˜oes, em vez de λ-express˜oes, 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 est´agio intermedi´ario das λ-express˜oes. A m´aquina de redu¸c˜ao de Turner foi chamada de M´aquina de Redu¸c˜ao SK e utilizava a redu¸c˜ao em grafos como m´etodo para sua implementa¸c˜ao. Esta m´aquina teve um impacto muito intenso na implementa¸c˜ao de linguagens aplicativas, pelo ganho em eficiˆencia, uma vez que ela n˜ao utilizava o ambiente da m´aquina SECD, mas tirava partido do compartilhamento que conseguia nos grafos de redu¸c˜ao.

Um outro pesquisador que se tornou famoso no desenvolvimento de m´aquinas abstratas foi Johnsson, a partir de 1984, quando ele deu in´ıcio a uma s´erie de publica¸c˜oes [16, 17, 18] sobre este tema, culminando com sua Tese de Doutorado, em 1987 [19], onde ele descreve uma m´aquina abstrata baseada em supercombinadores que eram combinadores abstra´ıdos do programa de usu´ario para algumas necessidades particulares. A m´aquina inventada por Johnsson ficou co- nhecida pela M´aquina G e se caracterizou por promover uma melhoria na granularidade dos programas, que era muito fina na m´aquina de redu¸c˜ao de Turner. Uma otimiza¸c˜ao importante desta m´aquina foi a utiliza¸c˜ao de uma segunda pilha na avalia¸c˜ao de express˜oes aritm´eticas ou outras express˜oes estritas.

Figura 4: O processo de compila¸c˜ao das linguagens funcionais adotado em ΓCMC.

V´arias outras m´aquinas abstratas foram constru´ıdas com bons resultados. Entre elas podem ser citadas a m´aquina GMC e a m´aquina Γ, idealizadas por Rafael Lins, da Universidade Federal

(^5) Combinadores e supercombinadores s˜ao temas do λ-c´alculo, objeto de estudo do Cap´ıtulo 2, deste trabalho.

de Pernambuco [8].

Al´em destas, uma que tem se destacado com excelentes resultados ´e a m´aquina ΓCMC, tamb´em de Rafael [21, 8], onde um programa codificado em SASL ´e traduzido para um programa em Ansi C. Este processo est´a mostrado na Figura 4.

A escolha da linguagem C se deve ao fato de que os compiladores de C geram c´odigos reco- nhecidamente port´ateis e eficientes. A m´aquina ΓCMC ´e baseada nos combinadores categ´oricos que se fundamentam na Teoria das Categorias Cartesianas Fechadas, recentemente utilizada em diversas ´areas da Computa¸c˜ao, 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 Introdu¸c˜ao e 6 (seis) Cap´ıtulos. Nesta Introdu¸c˜ao, ´e colocada, de forma resumida, a importˆancia das linguagens funcionais e a necessidade de estudar o λ- c´alculo e justificar sua escolha como a linguagem intermedi´aria entre as linguagens funcionais e as linguagens de m´aquina. E necess´´ ario saber que as linguagens funcionais s˜ao importantes porque aumentam a modularidade dos sistemas atrav´es das fun¸c˜oes de alto n´ıvel e do mecanismo de avalia¸c˜ao pregui¸cosa.

O Cap´ıtulo 1 ´e dedicado `a fundamenta¸c˜ao das linguagens funcionais, abordando as princi- pais diferen¸cas entre elas e as linguagens de outros paradigmas. O mundo das linguagens de programa¸c˜ao ´e dividido entre o mundo das express˜oes e o mundo das atribui¸c˜oes, evidenciando as vantagens do primeiro mundo em rela¸c˜ao ao segundo.

No Cap´ıtulo 2, ´e introduzido o λ-c´alculo, sua evolu¸c˜ao hist´orica e como ele ´e usado nos dias atuais. A teoria ´e colocada de maneira simples e introdut´oria, dado o objetivo da Apostila.

No Cap´ıtulo 3, inicia-se a programa¸c˜ao em Haskell. S˜ao mostrados seus construtores e uma s´erie de exemplos, analisando como as fun¸c˜oes podem ser constru´ıdas em Haskell. S˜ao mostrados os tipos de dados primitivos adotados em Haskell e os tipos estruturados mais simples que s˜ao as tuplas. No Cap´ıtulo, tamb´em s˜ao mostrados os esquemas de provas de programas, juntamente com v´arios exerc´ıcios, resolvidos ou propostos.

O Cap´ıtulo 4 ´e dedicado a listas em Haskell. Este Cap´ıtulo se torna necess´ario, dada a importˆancia que este tipo de dado tem nas linguagens funcionais. Neste Cap´ıtulo, s˜ao mostradas as compreens˜oes ou express˜oes ZF e tamb´em ´e mostrada a composi¸c˜ao de fun¸c˜oes como uma caracter´ıstica apenas das linguagens funcionais, usada na constru¸c˜ao de fun¸c˜oes. Um tema importante e que ´e discutido neste Cap´ıtulo se refereas formas de provas da corretude de programas em Haskell, usando indu¸c˜ao estrutural sobre listas. No Cap´ıtulo s˜ao mostrados v´arios exemplos resolvidos e, ao final, s˜ao colocados v´arios exerc´ıcios `a aprecia¸c˜ao do leitor.

No Cap´ıtulo 5, s˜ao mostradas as type class, como formas de incluir um determinado tipo de dados em em uma classe de tipos que tenham fun¸c˜oes em comum, dando origem `a sobrecarga como forma de polimorfismo. Tamb´em s˜ao mostrados os tipos de dados alg´ebricos, o sistema de m´odulos adotado em Haskell, os tipos de dados abstratos e o tratamento de exce¸c˜oes. O Cap´ıtulo termina com uma revis˜ao sobre o sistema de avalia¸c˜ao lazy, notadamente na constru¸c˜ao de listas potencialmente infinitas.

O Cap´ıtulo 6 ´e dedicado `as opera¸c˜oes de entrada e sa´ıda em Haskell, evidenciado o uso de arquivos ou dispositivos como sa´ıda ou como entrada. Este processo em Haskell ´e feito atrav´es do mecanismo de “a¸c˜oes”, cuja semˆantica representa o conte´udo principal do Cap´ıtulo.

A Apostila termina com as principais referˆencias bibliogr´aficas consultadas durante s sua elabora¸c˜ao.

Cap´ıtulo 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 programa¸c˜ao funcional teve in´ıcio antes da inven¸c˜ao dos computadores eletrˆonicos. No in´ıcio do s´eculo XX, muitos matem´aticos estavam preocupados com a fundamenta¸c˜ao matem´atica, em particular, queriam saber mais sobre os conjuntos infinitos. Muito desta preocupa¸c˜ao aconteceu por causa do surgimento, no final do s´eculo XIX, de uma teoria que afirmava a existˆencia de v´arias ordens de infinitos, desenvolvida por George Cantor (1845-1918) [24]. Muitos matem´aticos, como Leopold Kronecker (1823-1891), questionaram a existˆencia destes objetos e condenaram a teoria de Cantor como pura “enrola¸c˜ao”. Estes matem´aticos defendiam que um objeto matem´atico s´o poderia existir se, pelo menos em princ´ıpio, pudesse ser constru´ıdo. Por este motivo, eles ficaram conhecidos como “construtivistas”.

Mas o que significa dizer que um n´umero, ou outro objeto matem´atico, seja construt´ıvel? Esta id´eia foi desenvolvida lentamente, ao longo de muitos anos. Guiseppe Peano (1858-1932), um matem´atico, l´ogico e linguista, escreveu “Formulaire de Math´ematique” (1894-1908), onde mostrou como os n´umeros naturais poderiam ser constru´ıdos atrav´es de finitas aplica¸c˜oes da fun¸c˜ao sucessor. Come¸cando em 1923, Thoralf Skolen (1887-1963) mostrou que quase tudo da teoria dos n´umeros naturais poderia ser desenvolvido construtivamente pelo uso intensivo de defini¸c˜oes recursivas, como as de Peano. Para evitar apelos question´aveis sobre o infinito, pareceu razo´avel chamar um objeto de construt´ıvel se ele pudesse ser constru´ıdo em um n´umero finito de passos, cada um deles requerendo apenas uma quantidade finita de esfor¸co. Assim, nas primeiras d´ecadas do s´eculo XX, j´a existia consider´avel experiˆencia sobre as defini¸c˜oes recursivas de fun¸c˜oes sobre os n´umeros naturais.

A cardinalidade (quantidade de elementos) dos conjuntos finitos era f´acil de ser conhecida, uma vez que era necess´ario apenas contar seus elementos. J´a para os conjuntos infinitos, esta t´ecnica n˜ao podia ser aplicada. Inicialmente, era necess´ario definir o que era realmente um conjunto infinito. Foi definido que um conjunto era infinito se fosse poss´ıvel construir uma correspondˆencia biun´ıvoca entre ele e um subconjunto pr´oprio dele mesmo. Foram definidos os conjuntos infinitos enumer´aveis, que foram caracterizados pelos conjuntos infinitos para os quais

11

fosse poss´ıvel construir uma correspondˆencia biun´ıvoca com o conjunto dos n´umeros naturais, N. Assim, todos os conjuntos infinitos enumer´aveis tinham a mesma cardinalidade, que foi definida por ℵ 01. Assim, a cardinalidade do conjunto dos n´umeros inteiros, Z, tamb´em ´e ℵ 0 , uma vez que ´e poss´ıvel construir uma correspondˆencia biun´ıvoca entre Z e N. Os conjuntos infinitos com os quais n˜ao fosse poss´ıvel estabelecer uma correspondˆencia biun´ıvoca entre eles e N, foram chamados de infinitos n˜ao enumer´aveis. A cardinalidade do conjuntos dos n´umeros reais, R, que ´e infinito n˜ao enumer´avel, ´e 2ℵ^0.

1.2 Hist´orico

Na d´ecada de 1930, existiram muitas tentativas de formaliza¸c˜ao do construtivismo, procurando caracterizar o que era camputabilidade efetiva, ou seja, procurava-se saber o que realmente podia ser computado. Uma das mais famosas tentativas foi a defini¸c˜ao de Turing sobre uma classe de m´aquinas abstratas, que ficaram conhecidas como “m´aquinas de Turing”, que realizavam opera¸c˜oes de leitura e escritas sobre uma fita de tamanho finito. Outra t´ecnica, baseada mais diretamente nos trabalhos de Skolen e Peano, consistia no uso de “ fun¸c˜oes recursivas gerais”, devida a G¨odel. Uma outra t´ecnica, com implica¸c˜ao importante na programa¸c˜ao funcional, foi a cria¸c˜ao do λ-c´alculo, desenvolvido por Church e Kleene, no in´ıcio da d´ecada de 1930. Outra no¸c˜ao de computabilidade, conhecida como “Algoritmos de Markov”, tamb´em foi desenvolvida nesta mesma ´epoca. O que ´e importante ´e que todas estas no¸c˜oes de computabilidade foram provadas serem equivalentes. Esta equivalˆencia levou Church, em 1936, a propor o que ficou conhecida como a Tese de Church^2 , onde ele afirmava que “uma fun¸c˜ao era comput´avel se ela fosse primitiva recursiva” [5].

Isto significa que, j´a na d´ecada anterior `a d´ecada da inven¸c˜ao do computador eletrˆonico, muitos matem´aticos e l´ogicos j´a haviam investigado, com profundidade, a computabilidade de fun¸c˜oes e identificado a classe das fun¸c˜oes comput´aveis como a classe das fun¸c˜oes primitivas recursivas.

O pr´oximo invento importante na hist´oria da programa¸c˜ao funcional foi a publica¸c˜ao de John McCarthy, em 1960, sobre LISP. Em 1958, ele investigava o uso de opera¸c˜oes sobre listas ligadas para implementar um programa de diferencia¸c˜ao simb´olica. Como a diferencia¸c˜ao ´e um processo recursivo, McCarthy sentiu-se atra´ıdo a usar fun¸c˜oes recursivas e, al´em disso, ele tamb´em achou conveniente passar fun¸c˜oes como argumentos para outras fun¸c˜oes. McCarthy verificou que o λ-c´alculo provia uma nota¸c˜ao muito conveniente para estes prop´ositos e, por isto mesmo, ele resolveu usar a nota¸c˜ao de Church em sua programa¸c˜ao.

Em 1958, foi iniciado um projeto no MIT com o objetivo de construir uma linguagem de programa¸c˜ao que incorporasse estas id´eias. O resultado ficou conhecido como LISP 1, que foi descrita por McCarthy, em 1960, em seu artigo “Recursive Functions of Symbolic Expressions and Their Computation by Machine” [24]. Este artigo mostrou como v´arios programas com- plexos podiam ser expressos por fun¸c˜oes puras operando sobre estruturas de listas. Este fato ´e caracterizado, por alguns pesquisadores, como o marco inicial da programa¸c˜ao funcional.

No final da d´ecada de 1960 e in´ıcio da d´ecada de 1970, um grande n´umero de cientistas da Computa¸c˜ao come¸caram a investigar a programa¸c˜ao com fun¸c˜oes puras, chamada de “pro- grama¸c˜ao aplicativa”, uma vez que a opera¸c˜ao central consistia na aplica¸c˜ao de uma fun¸c˜ao a seu argumento. Em particular, Peter Landin (1964, 1965 e 1966) desenvolveu muitas das id´eias centrais para o uso, nota¸c˜ao e implementa¸c˜ao das linguagens de programa¸c˜ao aplicativas, sendo importante destacar sua tentativa de traduzir a defini¸c˜ao de uma linguagem n˜ao funcional, Algol

(^1) ℵ ´e uma letra do alfabeto ´arabe, conhecida por Aleph. (^2) Na realidade, n˜ao se trata de uma tese, uma vez que ela nunca foi provada. No entanto, nunca foi exibido um contra-exemplo, mostrando que esta conjectura esteja errada.

12