Tutorial de Programação Haskell

De Aulas

Links Relacionados: Linguagens de Programação, Paradigma de Programação Funcional.

Introdução

Haskell é uma linguagem de programação funcional (anos 80) que tem como base o cálculo lambda. Ela possui algumas das mais recentes características das linguagens funcionais como lazy evaluation, pattern matching e modularidade. A linguagem também permite que seus programas possam ser tanto compilados como interpretados. Os aplicativos da linguagem (ferramentas de compilação, debug, interpretação, etc.) são opensources e possuem versões para Windows, Linux e MacOSX.

Noções Preliminares

Um programa em Haskell deve começar com a linha module Name where, em que Name é o nome deste módulo, que tem que começar por letra maiúscula. O nome do arquivo deverá ser Name.hs, embora não seja obrigatório, será necessário caso queiramos importar este módulo noutro programa.

Os comentários são definidos por -- no início da linha, e os comentários em blocos (mais de uma linha) iniciam com {- e terminam com -}.

Os tipos começam por letras maiúsculas, tais como: Int, Integer, Float, Double, Char e Bool.

Primeiro Programa

1module Primeiro where
2
3quadrado :: Int -> Int
4quadrado x = x^2

Ainda não é nesse momento que você verá um "Hello World". Isso porque mostrar uma mensagem no monitor é um pouco complexo.

Para testar o programa basta salvar o arquivo com o nome Primeiro.hs e executar o comando ghci Primeiro.hs. Ao entrar no modo execução, digite, por exemplo:

1quadrado 5

O programa irá mostrar o resultado 25, de 5 elevado ao quadrado. Você pode continuar testando a função para outros valores. Para sair do interpretador, digite CTRL+D.

Um Exemplo mais Avançado

Como podem ver é muito simples definir funções matemáticas em Haskell. Deixo-vos mais alguns exemplos:

 1module Segundo where
 2
 3f1 :: Float -> Float -> Bool
 4f1 x y = x * y > 0
 5
 6divInt :: Int -> Int -> Int
 7divInt a b = if b = 0 then a else div a b
 8
 9resto :: Int -> Int -> Int
10resto a b = if b /= 0 then a `mod` b else 0
11
12soma x y = (+) x y
13
14-- verifica se um caracter é uma vogal
15
16vogal :: Char -> Bool
17vogal a = if a='a' || a='e' || a='i' || a='o' || a='u'
18	then True
19	else False
20
21f2 :: (Float, Float, Float) -> Float -> Bool
22f2 (a, b, c) d = sin ((a * b) / c) > d

Observações do exemplo apresentado acima:

  • Um if necessita de um else sempre.
  • Na função resto pode-se ver como transformar um operador prefixo (mod) em um infixo. Para isso, basta colocá-lo entre acentos graves.
  • Operadores infixos também podem passar para prefixos por meio de parênteses, tal como mostrado na função soma.
  • Na função vogal o then passa para a linha seguinte. É possível fazer isso, desde que haja identação, pelo menos um caractere.
  • Quando temos, por exemplo, uma função do tipo Int->Int->Int, podemos considerar que a função recebe dois inteiros e devolve um inteiro. Entretanto, a questão é mais complicada que isso. Isso será visto em um módulo mais avançado.

Para testar as funções, faça, por exemplo:

1resto 14 3
2vogal 'b'
3f2 (1,2,3) 0
4-- etc...

Mais sobre Condicionais

Existem várias formas de se utilizar condicionais em Haskell. As funções apresentadas abaixo, por exemplo, verificam se um valor é 0. Elas são todas equivalentes:

 1isZero a = if a = 0 then True else False
 2
 3isZero a | a = 0 = True
 4         | a /= 0 = False
 5
 6isZero a | a = 0 = True
 7         | otherwise = False
 8
 9isZero a = case a of
10           0 -> True
11           x -> False
12
13{-
14   Exemplos utilizando pattern matching. Neste caso, o caractere
15   _ é um coringa que pode representar 'qualquer coisa'
16-}
17
18isZero a = case a of
19           0 -> True
20           _ -> False
21
22isZero 0 = True
23isZero _ = False

Recursividade

Tal como na forma das linguagens funcionais, Haskell também não possui ciclos. Para tal, utiliza-se recursividade.

1factorial n = if n = 0 then 1 else n * factorial (n-1)
2
3quadrado n = if n = 0 then 0 else quadrado (n-1) + 2 * n - 1

Listas

Haskell suporta a utilização de listas. Entretanto, todos os elementos devem ser do mesmo tipo.

 1comprimento l = length l
 2
 3vazia [] = True
 4vazia _  = False
 5
 6-- A função head devolve o primeiro elemento da lista
 7primeiro :: [Int] -> Int
 8primeiro lista = head lista
 9
10-- A função tai devolve o último elemento da lista
11cauda [] = []
12cauda lista = tail lista
13
14-- A operação ++ junta duas listas
15junta :: [Int] -> [Int] -> [Int]
16junta lista1 lista2 = lista1 ++ lista2
17
18-- A operação : adiciona um elemento no início da lista
19adiciona :: Int -> [Int] -> [Int]
20adiciona x l = x : l
21
22- Se a lista estiver vazia, retorna 0
23somatorio [] = 0
24{-
25   Caso a lista se enquadrar no padrão 'x:xs', ou seja, um primeiro
26   elemento x e uma cauda xs, sendo que a cauda pode ser vazia, então
27   faz o somatório da cauda e soma com o início da lista.
28-}
29somatorio (x : xs) = x + (somatorio xs)

Abaixo são apresentados alguns exemplos de criação de listas:

 1-- lista dos inteiros de 1 a 100
 2[1..100]
 3
 4-- lista dos números pares maiores do que zero e menores do que 100
 5[2,4..100]
 6
 7-- lista dos quadrados dos números inteiros de 1 a 20
 8[n^2 | n <- [1..20]]
 9
10-- lista [(1,0),(2,1),(3,0),...]
11[(n,ispar) | n <- [1..20] , ispar <- [0,1] , ((mod n 2=0 && ispar=1) || (odd n && ispar=0))]
12
13-- lista de todas as combinações de elementos de [1,2,3] e de [4,5,6]
14[(x,y) | x <- [1,2,3] , y <- [4,5,6]]
15
16-- lista de todos os números inteiros maiores do que 0 (Isso deve ser muito! :) )
17-- Na verdade, Haskell pode possuir listas infinitas.
18[1..]

Existem algumas funções interessantes sobre listas, como por exemplo map, filter e foldl. Seguem alguns exemplos:

 1vezes2 n = n * 2
 2
 3-- aplica a função 'vezes2' a todos o elementos de uma lista
 4duplica :: [Int] -> [Int]
 5duplica l = map vezes2 l
 6
 7-- ou alternativamente
 8duplica l = map (* 2) l
 9
10-- filtra todos os elementos diferentes de 0 (usa a função 'isZero' definida anteriormente)
11naoZeros l = filter (not . isZero) l

Pode-se ver também a aplicação da composição de funções ('.'): (not . isZero) n é equivalente a not (isZero n).

Strings

As strings, tal como em diversas outras linguagens, é uma lista de caracteres.

1juntaLetra :: Char -> String -> [Char]
2juntaLetra l str = l : str
3
4juntaNoFin l str = str ++ [l]
5
6-- a função 'take' retorna os n primeiro elementos da string (lista de caracteres)
7dezLetras str = take 10 str
8
9comprimento str = length str

Sinônimos e Tipos de Dados definidos pelo Usuário

Em Haskell podemos definir sinônimos de tipos. Por exemplo, podemos associar ao nome Par o tipo (Int, Int). Para tal usamos a declaração type:

1type Par = (Int, Int)

Desta forma escrever Par ou (Int, Int) é a mesma coisa. No próximo exemplo as funções f1 e f2 são exatamente a mesma coisa.

1f1 :: (Int, Int) -> Int
2f1 (x,y) = x + y
3
4f2 :: Par -> Int
5f2 (x,y) = x + y

Como já foi referido, strings em Haskell são lista de caracteres, ou seja, foram declaradas da seguinte forma: type String = [Char].

Mas podemos fazer muito mais do que definir sinônimos em Haskell. Podemos definir novos tipos através da declaração data. Por exemplo, quando fazemos a divisão de dois números o denominador não pode ser zero, caso isso aconteça é um erro. Então vamos definir um tipo de dados que permite representar um inteiro e situações de erro e algumas funções que usam esse tipo.

 1data ResDiv = Erro | Res Int
 2
 3divisao :: Int -> Int -> ResDiv
 4divisao _ 0 = Erro
 5divisao n d = Res (div n d)
 6
 7mult :: ResDiv -> ResDiv -> ResDiv
 8mult Erro    _       = Erro
 9mult _       Erro    = Erro
10mult (Res x) (Res y) = x * y
11
12-- converte um valor do tipo ResDiv para Int.
13-- o valor erro é considerado 0
14res2int :: ResDiv -> Int
15res2int Erro    = 0
16res2int (Res x) = x

Neste caso o tipo ResDiv só iria servir se tivéssemos trabalhado com inteiros. Mas podemos generalizá-lo para qualquer tipo de dados.

 1data ResDiv a = Erro | Res a
 2
 3divisao :: Int -> Int -> ResDiv Int
 4divisao _ 0 = Erro
 5divisao n d = Res (div n d)
 6
 7mult :: ResDiv Float -> ResDiv Float -> ResDiv Float
 8mult Erro    _       = Erro
 9mult _       Erro    = Erro
10mult (Res x) (Res y) = x * y
11
12-- converte um valor do tipo ResDiv para Int.
13-- o valor erro é considerado 0
14res2int :: ResDiv Double -> Double
15res2int Erro    = 0
16res2int (Res x) = x

No entanto nem era preciso definir-mos este tipo de dados. Já tínhamos o tipo Maybe a que faz exatamente a mesma coisa. A sua definição é a seguinte:

1data Maybe a = Just a | Nothing

A única diferença em relação ao tipo por nós definido são mesmo os nomes. Mas também podemos ter tipos recursivos. Vejamos como definir expressões matemáticas e uma função que as avalia.

 1data Exp a = Val a                -- um numero
 2           | Neg (Exp a)          -- o simetrico de uma expressao
 3           | Add (Exp a) (Exp a)  -- a soma de duas expressoes
 4           | Sub (Exp a) (Exp a)  -- subtraccao de duas expressoes
 5           | Mul (Exp a) (Exp a)  -- multiplicacao ...
 6           | Div (Exp a) (Exp a)  -- divisao ...
 7
 8
 9avalia (Val x)          = x
10avalia (Neg exp)        = - (avalia exp)
11avalia (Add exp1 exp2)  = (avalia exp1) + (avalia exp2)
12avalia (Sub exp1 exp2)  = (avalia exp1) - (avalia exp2)
13avalia (Mul exp1 exp2)  = (avalia exp1) * (avalia exp2)
14avalia (Div exp1 exp2)  = (avalia exp1) / (avalia exp2)

Finalmente o Hello World

1main :: IO ()
2main = do putStrLn "Hello World"
3
4-- ou uma que retorna 0
5main :: IO Int
6main = do putStrLn "Hello World"
7         return 0

Referência