Você está na página 1de 11

Projeto e implementação da linguagem Haskell

Ubiratã Azevedo Ignácio


Universidade do Vale do Rio dos Sinos, Curso de Ciência da computação
São Leopoldo, Brasil
ubirata@skyinformatica.com.br

RESUMO

A linguagem Haskell tem obtido um grande reconhecimento e destaque tanto na área comercial quanto para a
utilização didática, no ensino de linguagens funcionais. Este artigo tem por objetivo descrever esta linguagem na
versão entitulada Haskell98, avaliando suas características no paradigma funcional, detalhes de implementação de
interpretadores e compiladores. Também será discutido questões de projeto da linguagem diante de conceitos de
linguagens de programação como tipos de dados, modularização, escopo de variáveis e etc, verificando o
comportamento da linguagem, assim como decisões tomadas em características específicas observadas nas
linguagens funcionais.

Palavras-chave: Paradigma funcional, linguagem funcional, Haskell

ABSTRACT

The Haskell language has been having a great importance, as into the commercial use , as into teaching functional
languages at schools and computer science courses. This paper intents to describe Haskell as it is defined in
version Haskell98, evaluating its issues in the craft of the functional paradigm. We will discuss language project
aspects especially denoting programming language concepts like data types, modularization, variable scope and etc,
verifying the language behavior , as well as decisions taken in especial issues of functional languages.

Keywords: Functional paradigm, functional language, Haskell

1. INTRODUÇÃO

Haskell é uma linguagem puramente funcional de aplicação geral, que incorpora muitas inovações recentes no que
diz respeito a projetos de linguagens de programação. Provê funções de ordem superior, semântica não restritiva,
polimorfismo estático para tipos, tipos de dados algébricos que podem ser criados pelo usuário, reconhecimento de
padrões, manipulação de listas, e um rico conjunto de tipos primitivos de dados, incluindo arrays, inteiros de
precisão arbritária e números em ponto flutuante. Haskell é tanto uma aglutinação como a solidificação de anos de
pesquisa em linguagens funcionais.

O desenvolvimento da linguagem Haskell teve início em 1987. Atualmente a definição da linguagem Haskell se
encontra na versão Haskell98. É neste estágio de desenvolvimento da linguagem que será discutido os seus
aspectos de implementação e características de projeto. O objetivo não é demonstrar como se constrói algoritmos e
programas em Haskell, apenas serão analisados detalhes internos da construção da linguagem. No entanto serão
colocados algumas partes de código Haskell para demonstração e ilustração de exemplos.

2. O PROJETO HASKELL

Em setembro de 1987 um evento na conferência de Linguagens de Programação Funcionais e Arquitetura de


Computadores (FPCA '87) em Portland, Oregon, tratou de um assunto peculiar na comunidade de linguagens de
programação funcional: uma grande quantidade de linguagens, puramente funcionais, não restritiva e com grande
poder de expressão estavam sendo concebidas. Houve então um forte consenso de que esta grande variedade de
linguagens poderia tornar difícil a adoção de uma linguagem comum, assim, foi formado um comitê com o objetivo
de projetar tal linguagem, tornando possível ter uma base estável para desenvolvimento real de aplicações no
paradigma funcional, encorajando o crescimento da utilização de linguagens funcionais.

Assim nascia o Haskell, carregando o nome de Haskell B. Curry, um dos pioneiros em cálculo lâmbda. Os
objetivos da linguagem eram os seguintes:
a. deveria facilitar o ensino de linguagens funcionais, pesquisa e criação de aplicações, incluindo grandes projetos
de sistemas.
b. a linguagem deveria ser completamente descrita com sintaxe e semântica formais.
c. ser baseada em idéias de grande consenso comum e reduzir a necessidade de uma grande diversidade de
linguagens funcionais.

Com a adoção do projeto, várias versões do Haskell foram sendo publicadas através dos anos, até que em 1998 foi
lançada a versão que consolidaria de vez a utilização de Haskell como uma poderosa linguagem de programação no
paradigma funcional.

2.1 Haskell como Linguagem de Programação

Nas sessões seguintes abordaremos mais detalhadamente alguns pontos importante do Haskell, tanto do ponto de
vista do usuário programador, como aspecto de implementação da linguagem. Na sessão 3, será feita uma análise
da estrutura de um programa em Haskell, forma de declaração de variáveis e funções, identificadores e operadores.
Na sessão 4 serão explanados os tipos de dados primitivos, os que podem ser definidos pelo usuário e como o um
compilador/interpretador Haskell deve se comportar na verificação e vinculação de tipos de dados. Já ne sessão 5,
discutiremos como o Haskell implementa alguns aspectos de orientação a objeto através de classes e sobrecarga. A
sessão 6 deste artigo define e estuda o modelo de verificação de tipo adotados pelos compiladores e interpretadores
da linguagem. Na sessão 7 veremos mais um ponto da versatilidade do Haskell analisando a forma com que a
linguagem implementa tratamento excessões/

3. ESTRUTURA DE UM PROGRAMA HASKELL

Nesta sessão veremos como se estrutura um programa em Haskell, os aspectos relevantes a característica funcional
da linguagem e alguns detalhes de implementação, do ponto de vista do programador.

3.1 Scripts de Programas e Módulos

Como uma linguagem funcional, o centro da organização de um programa está definido nas funções que são
criadas, porém, existem outros fatores importantes a serem observados.
Nas primeira linhas do arquivo que conterá o programa são descritos os conjuntos de módulos que serão utilizados
neste programa. Estes módulo podem ser tanto arquivos criados pelo próprio usuário, como outros que são padrões
em um ambiente Haskell (que são chamados de standard prelude). Isto permite uma separação e organização mais
fácil e simplificada de projetos que acabam ser tornando muito grandes.
Para que se possa utilizar um módulo dentro de um programa, basta adicionar no inicio do arquivo a seguinte
declaração:

import <nome do módulo> as <nome_especifico>

Pode-se informar mais de um módulo a ser importado, se desejado. Módulos pertencentes ao standard prelude não
necessitam serem importados explicitamente. A forma de acesso a um módulo pode ser redefinida, criando um alias
para este com a palavra as, como exemplificado acima.

Já a criação de módulos possui detalhes específicos. Módulos não devem ser executados como programas e sua
estrutura é diferente de tais. No cabeçalho de um módulo deve ser identificar o nome do módulo, quais funções este
está exportando e, no seu corpo, então são descritas estas funções.
O cabeçalho de um módulo é descrito na seguinte forma:

module <nome do módulo> where


<import <nome do módulo>
<declaração das funções a serem exportadas>
<declaração das variáveis a serem exportadas>
corpo do módulo

Note que é possível importar outros módulos para serem utilizados dentro de um mesmo módulo. No corpo do
módulo é onde são descritas as funções disponíveis. É possível ter vários módulos num mesmo arquivo, porém não
é uma prática recomendada.
3.2 Definições de Tipos e Funções

Um programa Haskell consiste num número de definições. Estas definições podem ser associadas a um nome (ou
identificador) de um tipo particular. Existem seis tipos de nomes no Haskell: para variáveis e construtores que
denotem valor, nomes para tipos de variáveis, tipos de construtores, tipos de classes e nomes de módulos. Para esta
nomenclatura, existem três restrições a serem respeitadas:

- nomes para variáveis e variáveis tipos devem iniciar com letras minúsculas ou sinal de sublinhado
Para todos os outros tipos de nomes (tipos de dados, classes), deve-se iniciar com letras maiúsculas, seguidas ou
não de letras minúsculas
- operadores de construtores são definidos iniciando com ":", já os operadores de variáveis não iniciam
com ":"
- não deve-se utilizar um operador com o mesmo nome de um tipo ou classe, no mesmo escopo
No caso de definição simples, podemos observar no exemplo abaixo:

nome : : tipo
nome = expressão

a : : Int
a = 2+5

A formulação nome :: tipo determina de que tipo uma variável (ou função) deve ter (: : deve ser lido como "é do
tipo"). Podemos notar no exemplo acima que a variável a foi definida com o tipo de dado primitivo Int (que denota
inteiros), e logo após a esta variável foi atribuído um valor resultado da expressão 2+5, tendo o valor 7.

Para definir funções no Haskell, podemos utilizar a mesma analogia usada para variáveis. Uma função deve receber
parâmetros e deve retornar um resultado de um determinado tipo de dados. Após declarada, esta função deve ter um
corpo que implementa a avaliação da função sobre os parâmetros. Por exemplo:

quadrado : : Int -> Int


quadrado n = n*n

Temos a função quadrado que recebe um inteiro como parâmetro e retorna outro inteiro. A formulação tipo -> tipo
identifica os argumentos formais da função e em qual tipo ela deve retornar. Após a declaração da função, temos
sua implementação na forma <nome da função ><corpo>. No caso acima o nome da função é quadrado e o seu
corpo (que define a sua execução e resultado) é n = n*n. Como n pode ser um valor arbitrário, qualquer valor
passado a função será colocado no lugar de n. Para definir funções com n parâmetros formais, basta encadear de
seguinte forma:

x : : Int -> Int -> Float -> Bool -> Int

A função de nome x possui 4 argumentos (inteiro, inteiro, float e booleano) e retorna um valor inteiro. O último
tipo especificado identifica o tipo de dado a ser retornado. Os tipos de dados serão mais detalhadamente discutidos
na próxima sessão.

Uma característica interessante na definição de funções no Haskell são os Guards. Estes exploram a capacidade de
se inserir condições que são utilizadas como alternativas para a função. Um guard é uma expressão booleana, que é
utilizada para selecionar vários casos na definição de uma função, de acordo com a veracidade da expressão.
Observamos o seguinte exemplo, onde a função max retorna o maior valor entre dois números.:

max : : Int -> Int -> Int


max x y
| x >= y =x se x >= y então retorne x
| otherwise =y caso contrário, retorne y

No caso acima, caso o primeiro guard (x >=y) seja verdadeiro, o retorno da função é x. Por outro lado, caso seja
falso, deve-se procurar pelo segundo e assim por diante. Como não há outros guards, o função entra no caso que
indica generalidade, identificado pelo termo otherwise.

A sintaxe do Haskell permite que sejam utilizados operadores tanto na forma infixada como na forma pré fixada.
Isto permite que operadores possam ser utilizados da mesma forma que identificadores de funções, e além disto,
permite que possam ser alternados entre si, de forma transparente. Qualquer função ou identificador pode ser
convertido em um operador colocando-os entre sinal de crase e qualquer operador pode ser convertido em
identificador colocando entre parênteses. Por exemplo x+y é equivalente a (+) x y, e f x y é o mesmo que x `f` y.

Comentário de uma linha no Haskell devem iniciar por dois hífens (como -- ). Grupos de linhas a serem
comentadas devem iniciar com "{-" e encerrar com "- }".

Não por acaso, o Haskell nos dá um grande poder para trabalhar e processar funções. Além de existirem várias
formas de expressá-las, pode-se aplicar as funções de diversas maneiras. Uma delas é a composição de funções.

Para compor uma função f com a função g, devemos utilizar o operador ".", isto é, (f . g) x tem o mesmo significado
de f(g x). Note a importância dos parênteses para aninhar a aplicação das funções f e g. Caso expressarmos a
composição da forma f . g x simplesmente, isto resultaria em erro, pois o Haskell tentaria aplicar a função f sobre
um argumento g, que também é uma função, porém sem nenhum argumento específico. Um exemplo disto seria

not . not True

a expressão seria interpretada como tentar aplicar a função not tendo como parâmetro também um not o que não é
válido pois a função not espera um tipo booleano como parâmetro. Para resolver este problema teríamos de
expressar da forma (not . not) True.

Podemos realizar também a composição inversa de funções utilizando o operador ">.>". Assim, uma expressão do
tipo (f >.> g) x seria interpretada como g(f x).

4. TIPOS DE DADOS

Vimos na sessão anterior como se estrutura um programa ou módulo em Haskell. Feito isto, também observamos
como definir variáveis e seus tipos de dados. Nesta etapa, analisaremos com detalhes os tipos de dados suportados
pela linguagem e suas aplicações dentro do contexto de linguagem funcional.

O Haskell possui uma quantidade suficiente de tipos de dados primitivos, e mais importante ainda, uma grande
facilidade para que o usuário possa criar e definir seus próprios tipos de dados.

O tipo de dado booleano no Haskell é definido pela palavra Bool e pode ser comparados por igualdade ou com
operadores relacionais. No entanto, os operadores mais comuns para este tipo são

&& e
|| ou
not negação

Tipos de dados inteiros são identificados pela palavra Int e representam um número limitado de valores numéricos
inteiros. A variável maxBoud informa sobre a capacidade máxima do tipo que, neste caso é 2147483674. Os
operadores que podem ser utilizados com estes tipos de dados seguem desde operadores mais até alguns
particulares do Haskell. Abaixo veremos cada um e sua função

+ Soma
- Subtraçãp
* Multiplicação
^ Potência
div Divisão inteira
mod resto da divisão
abs valor absoluto de um inteiro
negate troca o sinal do valor

Note que para utilizar operadores como mod ou div na forma infixada, deve-se colocá-los entre sinais de crase.

Ambos os tipos Bool e Int podem ser comparados pelo operador de igualdade "==" mesmo sendo de tipos
diferentes. Isto acontece para todos os tipos de dados simples no Haskell, qualquer um pode ser comparado por
igualdade com "==". Este característica do Haskell já denota a capacidade da linguagem de manipular sobrecarga
de operadores . Poderemos ver estas propriedades com mais detalhes na próxima sessão.
No Haskell os tipos de dados caracteres são representados pela palavra Char. Variáveis deste tipo podem
armazenar caracteres literais representados por uma letra entre aspas simples (por exemplo a letra 'a'). Porém
também temos alguns caracteres com representações especiais como:
'\t' marca de tabulação
'\n' nova linhas
'\\' barra invertida \
'\'' aspas simples
'\"' aspas duplas

Existem duas funções interessantes para a manipulação deste tipo de dado. São elas ord, que retorna o código
ASCII do caracter e a função chr que toma um código ASCII e retorna o caracter correspondente.

Um String no Haskell nada mais é do que uma lista (tratada na próxima sessão) de caracteres do tipo Char.

Os números de ponto flutuantes são identificados como Float, para números que não necessitam de grande
precisão. Para representar valores mais precisos pode ser utilizado o tipo de dado Double, que significa dupla
precisão.

4.1 Tipos Abstratos de Dados, Listas e Tuplas

Observamos e analisamos os tipos de dados mais simples no Haskell, ditos primitivos. Porém, necessitamos de
formas de representar situações mais complexas para que possamos modelar problemas reais com certa facilidade.
Tanto tuplas como listas são construídas combinando pedaços de dados em um único objeto, porém com
propriedades diferentes. Numa tupla, são inseridas um número predeterminado de dados com tipos
predeterminados ( que podem ser diferentes), já numa lista se concentram um número arbitrário de valores de um
único tipo de dado. Para exemplificar, podemos definir uma tupla com os tipos String e Int que armazenariam
informações sobre um produto de supermercado e se valor correspondente. note que uma tupla é delimitada por
parênteses:

("Cereais", 10)
("Chocolate" , 3)

Numa lista poderíamos ter uma seqüência de valores do tipo Int delimitados por colchetes da seguinte forma:

[44, 4 , 55, 66, 3, 1]

Também é possível combinar ambos os tipos como em:

[("Cereais", 10), ("Chocolate", 3)]

Formalmente podemos definir as tuplas como

(t1, t2, t3, t4, ..., tn) onde tk representa o tipo de dado de cada item
(v1, v2, v3, v4, ..., vn) onde vn representa o valor de cada dado

Da mesma forma, as listas podem ser definidas como

[L ] : : [t] onde t é o tipo de dado da lista L


[v1, v2, v3, ...,vn] são os valores da lista. Todos de um mesmo tipo.

Para a criação de tipos de dados abstratos, definidos pelo usuário, utiliza-se do identificador type. Assim, um novo
tipo de dado poderia ser:

type MinhaLista : : [Int]

Criamos um novo tipo chamado MinhaLista, que representa uma lista de elementos de valor inteiro. Poderíamos
criar uma variável a como
a : : MinhaLista
e atribuir o conteúdo de uma lista a ela

a = [ 1, 23, 44, 21, ...]


5. CLASSES E SOBRECARGA

Além dos tipos de dados simples e os tipos de dados mais complexos como os abstratos vistos anteriormente, O
Haskell ainda permite que o usuário programador crie alguns outros tipos denominados Classes. As classes nesta
linguagem possuem um contexto semelhante ao significado de classes na orientação a objeto, porém, com algumas
características de implementação notavelmente diferentes.

Observando as funções no Haskell, notamos que muitas delas se aplicam a mais de um tipo de dado diferente. Uma
função polimórfica como length, possui uma única definição que pode ser utilizada para vários tipos de dados.
Funções sobrecarregadas como as de igualdade e soma podem ser utilizadas com diferentes tipos, porém também
com definições diferentes para cada tipo de dado empregado.
Nesta sessão discutiremos alguns aspectos de sobrecarga e polimorfismo no Haskell, assim como a utilização de
classes como tipos de dados.

5.1 Sobrecarga no Haskell

Supondo que não tivéssemos suporte a sobrecarga no Haskell e quiséssemos escrever uma função que nos
permitisse identificar quando um elemento está contido numa lista de tipo Bool. Deveríamos definir uma função
como:

elemBool : : Bool -> [Bool] -> Bool


elemBool x [] = False
elemBool x(y:s) = (x ==Bool y) || elemBool x ys

O que acontece no caso acima é que, estamos definindo uma função que recebe em seus argumentos um dado
booleano e uma lista com valores a serem comparados. A função recursiva define em x [] = False que quando a
lista estiver vazia, o resultado é falso. Do contrário, ou o elemento é o primeiro da lista, ou a a função é chamada
novamente com uma nova lista sem este primeiro elemento.

Note que o operador ==Bool indica uma comparação de igualdade entre dados do tipo Bool apenas. Agora,
supomos que gostaríamos de escrever uma função com a mesma característica, porém para dados do tipo Int. Para
isto, teríamos que utilizar o operador de comparação ==Int, que difere da função anterior, não permitindo
reaproveitamento do código.

Uma forma de resolver este problema, é utilizar um operador de igualdade para valores com tipos de dados não
definidos, como em:

elemGen : : (a -> a -> Bool) -> a -> [a] -> Bool

Porém, temos generalidade demais neste caso, e sem fazer sentido, pois esta função poderia ser aplicada sobre
quaisquer argumentos a -> a -> Bool, ao invés de apenas realizar um teste de igualdade.
Uma solução seria utilizar o operador de igualdade nativo do Haskell representado por "==" que suporta
sobrecarga. Desta forma, poderíamos definir funções simplesmente da seguinte maneira:

elem : : a -> [a] -> Bool

As vantagens óbvias deste tipo de mecanismo seria a capacidade de reutilização de código, uma vez que a função
elem poderia ser utilizada para qualquer tipo de dado suportado pelo operador de igualdade.

Para criar uma função utilizando o conceito de polimorfismo, basta declarar e definir as funções com o mesmo
nome, porém com os argumentos próprios para cara objetivo. Por exemplo a função length pode retornar tanto o
tamanho de uma String, quanto de uma Lista:

length : : String -> Int


length : : [a] -> Int

5.2 Classes no Haskell

As classes funcionam como tipos de dados abstratos e o usuário tem total liberdade para definir inúmeros tipos de
classes diferentes. A definição de uma classe consiste em:
class <nome_da_classe> <tipo_de_entrada> where
<assinatura da classe>

O nome da classe é o identificador único desta, já o tipo de entrada, pode ser uma palavra reservada que represente
um tipo de dado, ou apenas uma variável que atuaria como variável de tipo, fazendo com que a classe aceitem
qualquer tipo para a entrada. A assinatura da classe define as funções que poderão ser aplicadas aquela classe. Por
exemplo, poderíamos criar a seguinte situação.

Class V a where (1)


toString a : : a -> String
size a : : a -> Int

Onde temos em V, o nome da classe, em a uma variável de tipo que representa qualquer tipo de dado, e em sua
assinatura as funções toString, que converteria qualquer tipo de dado para um valor textual, e outra função size, que
retornaria o tamanho do tipo de dados recebido como argumento para a classe.

Podemos analisar a classe Eq, pré definida no Haskell, que representa as comparações de igualdade entre os tipos
de dados. A definição da classe Eq seria:

class Eq a where
(==) : : a -> a -> Bool

A classe Eq compara quaisquer tipos de dados através da função "==" e retorna a veracidade da comparação. Cada
instância desta classe implementa o mecanismo de comparação para um determinado tipo de dado.

Um membro de uma classe também é chamado de instância, e para instanciar um dado no Haskell, basta definir ar
funções de assinatura para o tipo de dado em questão. Para o exemplo da igualdade acima, instanciar um membro
do tipo Bool seria:

instance Eq Bool where (2)


True == True = True
False == False = True
_ == _ = False (que significa qualquer outra situação é falsa!)

A palavra chave instance define quando uma classe é instanciada em um novo membro. A definição em (2) denota
a execução para uma função de igualdade "==" para o tipo de dado Bool. Poderíamos ver um outro exemplo para a
classe V em (1):

instance V Char where


toString ch = [ch]
size _ =1

O membro da classe V é instanciado para o tipo de dado Char. A função toString retorna o argumento Char como
String, e a função size retorna 1 para qualquer entrada.

6. VERIFICAÇÃO DE TIPOS

Nas sessões anteriores vimos uma série de tipos de dados suportados pelo Haskell, desde os tipos mais simples e
próprios da linguagem, até mais complexos que podem ser criados pelo programador e terem diversos significados,
como as Classes. Assim, podemos entender que todos os valores em Haskell pertencem a um tipo de dado, que
pode ser Monomófico (apenas uma instância com mesmo nome), polimórfico (mais de uma instância com o mesmo
nome) ou pertencer a uma ou mais regras de definições de classes.

Para trabalhar com tamanha variedade de tipos com segurança, o Haskell implementa o mecanismo de checagem
forte de tipos de dados. Isto significa que as expressões que queremos avaliar, ou definições cujas regras de tipo
devam ser obedecidas, serão especialmente verificadas antes de serem executadas. É interessante observar que
tanto os compiladores quanto os interpretadores para esta linguagem utilizam o processo de verificação forte de
tipos.

Além disto, os tipos dos dados são um importante fator para a documentação de um programa. Quando observamos
uma definição, a primeira informação relevante que que percebemos é sobre seu tipo, que de uma certa forma
define como uma variável será utilizada. Nesta sessão, observaremos como é feita a verificação de tipo é feita sobre
características monomórficas e também sobre aspectos de implementação polimórfica.
6.1 Verificação de tipo monomórfica

Vamos agora então analisar como é feita a checagem de tipos monomóficos, isto é, sem polimorfismo ou
sobrecarga, e nosso objetivo principal é investigar a aplicação de funções de verificação de tipo.

Em geral, uma expressão pode ser um literal, uma variável ou constante, ou pode ser construído aplicando uma
função sobre seus argumentos, que são cada um também expressões. No caso de aplicação de funções, temos muito
mais do que poderíamos esperar em primeira mão. Por exemplo, observamos expressões de listas da forma [True,
False] como o resultado da aplicação da função de construção ":", sendo: True:[False]. Também operadores e
instruções do tipo if...then, são construídos exatamente da mesma forma do que as declarações de funções. Assim,
podemos ver a regra para a verificação de tipos na análise de uma função definida como f : : s -> t, onde a função f
deve ser aplicada sobre um argumento de tipo s e resultar numa expressão do tipo t. Para checar uma instrução f e,
o Haskell compreende a expressão da seguinte forma:

f e
| |
s -> t s
| |
----------------- f e ---------------
|
t

Um outro exemplo mais complexo mostra a aplicação da verificação em dois tipos de expressões. Utiliza-se a
função ord, que toma como argumento um Char e retorna um Int representando o código ASCII do caracter.

Ord 'c' + 3

A expressão como um todo teria uma definição do tipo Int1 -> Int2 -> Int3, onde Int1 representa a expressão ord
'c', Int2 o número literal 3 e Int3 o resultado. Porem, a expressão ord 'c' merece outra avaliação e sua definição é do
tipo Char -> Int. Aglutinando as informações poderíamos expressão a aplicação da função acima como

(Char -> Int) ->Int -> Int

Note que a verificação das expressões naturalmente acaba assumindo uma característica recursiva. O analisador de
expressões do compilador/interpretador deve descobrir o que pode ser uma expressão em Haskell e expandi-lá
numa árvore até que se obtenha a mais simples declaração dos tipos. Com esta informação, bastaria aplicar as
regras que estão descritas na definição da própria função e compará-las com os tipos de dados informados na
expressão raiz.

Para verificar expressões do tipo


f : : -> t1 -> t2 -> t3 ->...-> tn -> t
f p1 p2 p3...pn
| g1 = e1
| g2 = e2
...
|gk = ek

deve-se considerar os quesitos:


- onde cada valor dos guards gi devem ser do tipo Bool.
- o valor ei retornado deve ser do tipo t.
- p padrão de argumentos pj deve ser consistente com os tipos ti indicados na assinatura da função.

Encerrando o estudo do processo de verificação de tipos monomóficos, passamos para os tipos polimórficos.

6.2 Verificação de tipo polimórfica

Numa situação de monomorfismo, ou uma variável é bem tipada e pertence a um único tipo, ou ela não é tipada e
não pertence a nenhum tipo de dado. Já as situações que ocorrem no polimorfismo suportado pelo Haskell, as
coisas são mais complicadas, uma vez que um objeto polimórfico é exatamente um que possui vários tipos.

Para realizar a verificação de tipos em funções polimórficas, o Haskell lança mão do conceito de unificação. Assim
como em linguagens lógicas, uma unificação ocorre quando o analisador de expressões do compilador/interpretador
consegue determinar quais variáveis assumirão um determinado argumento genérico, e também de qual tipo de
dado será este argumento. Para esclarecer, observamos o exemplo:

soma : : a -> b -> c


soma a b = a + b

soma 6 3 = 9 6 unfica com a, 3 unifica com b e 9 unifica com c


soma 0.2 3.4 = 3.6 0.2 unfica com a, 4.3 unifica com b e 3.6 unifica com c

A função soma recebe dois argumentos que podem ser de tipos quaisquer, temos nos exemplos a chamada da
função com tipos de dados inteiros e também com tipos de ponto flutuante. Realizar a unificação sobre esta função
significa que, ao chamar soma, o primeiro argumento "fecha" com a, o segundo com b e o resultado com c. Isto
permite descobrir como posicionar os argumentos para uma função polimórfica. Agora vejamos um caso mais
complexo. Considere a função f abaixo:

f : : (a -> b) -> [a] -> [b]

Ao chamar f com parâmetros Char -> Int, a unifica com Char e b com Int. Isto vale para toda a expressão, assim
teremos, f : : (Char -> Int) -> [Char] -> [Int]. Note como o Haskell avalia expressões polimórficas, e também o
custo adicionado para realizar o processo de unificação.

Como já vimos anteriormente, a checagem de tipo está intrinsicamente ligada as expressões e suas declarações.
Vamos agora analisar os casos de polimorfismo em definições onde funções e constantes podem ser utilizadas de
formas diferentes na mesma expressão.

expr = length ([] ++ [True]) + length ([] ++[2,3,4])

A expressão acima define o resultado da soma do comprimento das listas obtidas em "([] ++ [True])" e na
expressão "([] ++ [234])". Cada uma destas expressões realizam a adição de um elemento qualquer a uma lista. A
primeira ocorrência de [] é interpretada como [Bool] e a segunda ocorrência como[Int]. Isto é completamente
válido e caracteriza uma das vantagens do polimorfismo. Agora, suponha que [] seja substituído por uma variável
como em:

func = length (xs ++ [True]) + length (xs ++[2,3,4])

A variável xs se comportaria como [Bool], mas também como [Int], isto significa que teríamos um polimorfismo
forçado neste caso, o que não é permitido pelo Haskell, pois não há como definir a função func, que deveria possuir
todos os tipos de instâncias como

func : : [Int] -> Int


func : : [[Char]] -> Int
...

que naturalmente não é possível devido a infinidade de instâncias possíveis. Podemos observar então que,
constantes como "[]" podem aparecer numa expressão com significados e tipos variados, já variáveis não podem ser
utilizadas desta forma.

7. TRATAMENTO DE EXCESSÕES NO HASKELL

Não muito comum em linguagens funcionais, o tratamento de excessões no Haskell é feito (nada mais
naturalmente) através de funções. Com a aplicação desta técnica é possível proteger a execução de funções
completas ou de apenas parte de uma expressão numa função, possibilitando que a execução desta função seja
tratada corretamente e que o processamento do código não seja interrompido. O modelo de excessões no Haskell é
o modelo de término de execução do código, que neste caso se aplica ao encerramento da execução de uma função.
Para utilizar este recurso, devemos compreender três identificadores no Haskell utilizados para o tratamento de
excessões e desvio de execução. Primeiramente a palavra catch define uma função que tem como primeiro
parâmetro a expressão a ser executada e como segundo parâmetro o tratador que deve ser disparado em caso de
ocorrer alguma excessão no processamento da primeira expressão.

f : : Int -> Int - >Float


fab=a/b

Na definição acima temos uma função f que divide dois números inteiros, resultando num valor de ponto flutuante.
Ao executarmos uma chamada desta função com uma variável x do tipo Int , a linguagem não tem como saber de
ante-mão quando a avaliação da expressão pode resultar numa divisão por zero.

x : : Int
f 5 (x -1)

No exemplo acima quando x tiver um valor superior a 1, o resultado da função poderá ser obtido. Caso contrário
uma excessão será gerada pelo Haskell, que possui um tratador de excessões genérico para todos os tipos de
excessão ocorrida, isto causará a interrupção da avaliação da função f. Porém, podemos proteger a execução de f da
seguinte forma

t : : -> 0
catch (f 5 (x-1)) t (3)

Definimos uma função t simples, que apenas retorna o número 0 como resultado. A chamada de catch (f 5 (x-1)) t
deve ser compreendida como tentar executar f 5 (x-1) e em caso de excessão, executar a função t. Note que o
tratamento de excessões não é seletivo no Haskell, assim em caso de erro executando f, não importando o tipo, a
função tratadora t será disparada. Naturalmente para isto o Haskell mantém internamente um tipo de dado
Exception que comporta os mais diversos erros possíveis de serem encontrados numa execução. Poderíamos
escrever o tratador em (3) de outras forma como

catch (f 5 (x-1)) (return 0)

em ambos os casos defininos que, em caso de erro executando f, o resultado da expressão será 0.

Podemos também proteger apenas pequenas partes de uma expressão como em:

g a x = a+5 *(catch (f x a) return 0)

A execução de g a x processa a + 5 normalmente e em caso de erro avaliando f x a, a função continuará tendo um


resultado, não interrompendo a execução do código.

As palavras-chave raise e throw são utilizadas para realizar definição de excessões. Podemos criar um tipo de
excessão com estes recursos, podendo então ter tipos de excessões definidas pelo próprio programador.

catch : : minha_excessao a (Exception -> minha_excessao a) -> minha_excessao -> a


raise : : Exception - > a
throw : : Exeption - > IO ->a

na primeira linha temos a definição de como será acionada a excessão pelo Haskell, isto é feito utilizando uma
chamada ao tipo nativo de tratamento de excessões no Haskell, Exception. Em caso de erro, o código definido em
"raise : :" será executado. Note que neste caso simplesmente estamos chamando o tratador padrão do Haskell para
a entrada a. O código em throw será executado apenas se a excessão gerada for do tipo entrada e saída, como
manipulação de arquivos, por exemplo.

O módulo no Haskell que permite realizar IO, tanto para arquivos como para console, possui uma série de funções
e tipo para a manipulação de excessões deste tipo. Isto é necessário, pois no Haskell até mesmo a leitura de final de
um arquivo pode gerar uma excessão.

8. CONCLUSÃO

Neste artigo tivemos a oportunidade de explanar os principais aspectos de implementação da linguagem funcional
Haskell. Além disso também foi discutido aspectos de programação do ponto de vista do usuário da linguagem. Em
geral as linguagens funcionais assumem um papel extremamente específico em projetos de desenvolvimento de
software, na maioria dos casos, aplicações com fins especiais conseguem tirar proveito do poder do paradigma
funcional. O Haskell extende esta aplicação suportando vários paradigmas na mesma linguagem, podendo-se
programar de forma funcional, imperativa ou orientada a objetos. Naturalmente este suporte sempre possui uma
base funcional e através dela se obtém uma abstração de outros paradigmas, porém é fácil notar toda a força do
Haskell que dá ao programador uma série de recursos de forma extremamente simples e principalmente didática,
possibilitando que a linguagem seja facilmente utilizada tanto no âmbito do ensino de programação em linguagens
funcionais, como na criação de grandes projetos de aplicativos comerciais mais complexos. Haskell nos dá todo o
poder do paradigma funcional com a facilidade de implementação e clareza de código.
REFERÊNCIAS BIBLIOGRÁFICAS

1. Thompson, Simon. Haskell: The Craft of Functional Programming. Second edition. Reading, Massachusetts:
Addison - Wesley

2. Simon L. Peyton Jones. The Implementation of Functional Programming Languages. Prientice Hall, 1987

3. L. Damas e R. Milner. Principal type schemes for funcional programs. In Proceedings of 9th ACM Symposium
on Principles of Programming Languages. Albuquerque, N.M., Janeiro 1982

4. P. Hudak, J. Fasel, e J. Peterson. A gentle introduction to Haskell. Technical Report YALEU/DSC/RR-901,


Yale University Maio 1996

5. J. Peterson (editor) . The Haskell Library Report. Technical Report YALEU/DSC/RR-1105, Yale University,
Maio 1996.

Você também pode gostar