Tutorial de Programação Haskell
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