Haskell/Casamento de padrões, if e let
Este módulo encontra-se em processo de tradução. A sua ajuda é bem vinda. |
Este capítulo introduz expressões if
, casamento de padrões e a palavra-chave let
.
if
/ then
/ else
editar
Haskell suporta expressões de condicionais simples, da forma se x então p senão q. A estrutura é a mesma que a de muitas linguagens de programação. Por exemplo, considere uma função que: retorna -1 e seu argumento for menor 0; 0 se o argumento for igual a 0; ou 1 se o argumento for maior que 0:
minhaSignum x =
if x < 0
then -1
else if x > 0
then 1
else 0
O fato é que Haskell já possui a função signum
que faz exatamente isso, mas vamos usá-la para exemplificar expressões condicionais.
Em qualquer expressão if
(if em inglês significa "se"), a primeira condição e avaliada: se ela for verdadeira, a expressão dentro de then
(then em inglês significa "então") é avalidada; se ela for falsa, a expressão dentro de else
(else em inglês significa "senão"), que pode ou não existir, é avaliada.
No caso de minhaSignum
:
- A condição avaliada é
x < 0
. - Se a condição retornar um Bool
True
, a expressão dentro dethen
é avaliada: a funçãominhaSignum
retornará-1
- Se a condição retornar um Bool
False
, a expressão dentro deelse
é avaliada: uma nova expressão if then else deve ser avaliada.- A condição avaliada é
x > 0
. - Se a condição retornar um Bool
True
, a expressão dentro dethen
é avaliada: a funçãominhaSignum
retornará1
; - Se a condição retornar um Bool
False
, a expressão dentro deelse
é avaliada: a funçãominhaSignum
retornará0
.
- A condição avaliada é
Aqui tivemos o exemplo de duas expressões condicionais aninhadas, isto é, uma dentro de outra, o que é perfeitamente possível e bastante comum em programação.[nota 1]
Qualquer função que precise usar expressões condicionais também pode ser escrita usando guardas:
minhaSignum x
| x < 0 = -1
| x > 0 = 1
| otherwise = 0
Da mesma forma, qualquer função que use guardas também pode ser escrita com expressões se explícitas. Vejamos a função absoluto
que definimos em Verdadeiro ou falso:
absoluto x =
if x < 0
then -x
else x
Mas por que há duas maneiras de se escrever expressões condicionais? Por que não usar somente if
ou somente guardas? Apesar de serem a mesma coisa, com o mesmo desempenho computacional, sua experiência lhe dirá qual das duas formas é mais legível. Não há melhor ou pior, só mais ou menos legível.
Tipos numa expressão condicional
editarÉ importante observar os tipos das expressões usadas dentro de if
/ then
/ else
:
- As condições avaliadas por
if
devem sempre retornar um Bool. - Os valores finais das expressões dentro de
then
eelse
, podem ser de qualquer tipo, mas ambos devem ser do mesmo tipo.
Esta é uma função válida em Haskell:
possivel x =
if x == 0
then "zero" -- then retorna um String
else "não-zero" -- else retorna um String
Esta é uma função inválida em Haskell:
impossivel x =
if x == 0
then 0 -- then retorna um Num
else "não-zero" -- else retorna um String
- Exercício
Por que as expressões dentro de then e else contidas num mesmo if devem possuir o mesmo tipo?
Casamento de padrões: introdução
editarConsidere um programa que analisa dados estatísticos de uma competição de corrida na qual cada piloto recebe pontos de acordo com sua posição final. As regras são:
- 1° lugar: 10 pontos
- 2° lugar: 6 pontos
- 3° lugar: 4 pontos
- 4° lugar: 3 pontos
- 5° lugar: 2 pontos
- 6° lugar: 1 pontos
- Nenhum ponto para os demais.
Podemos escrever uma função que recebe a classificação final de um piloto (representada por um número inteiro), e que retorna quantos pontos devem ser creditados.
pts :: Int -> Int
pts x =
if x == 1
then 10
else if x == 2
then 6
else if x == 3
then 4
else if x == 4
then 3
else if x == 5
then 2
else if x == 6
then 1
else 0
Por simplicidade, não vamos nos preocupar com o que aconteceria se o argumento fosse um número negativo, ou zero, por exemplo. Entretanto, numa aplicação real, deve-se sempre pensar nestes casos improváveis. Aqui simplesmente dissemos que o resultado é 0 para qualquer argumento que não seja maior que ou igual a 1 e menor que ou igual a 6.
Observe função que acabamos de escrever. Ela contem seis expressões ifaninhadas. Mesmo podendo copiar e colar texto, esta implementação ainda é propensa a erros, além de ser difícil de entender. Poderíamos escrever usando guardas, o que seria um pouco melhor, mas ainda seria necessário escrever todos os seis testes de igualdade. Fica como exercício.
- Exercício
Reescreva a função pts
usando guardas.
Uma maneira muito mais fácil e legível seria:
pts :: Int -> Int
pts 1 = 10
pts 2 = 6
pts 3 = 4
pts 4 = 3
pts 5 = 2
pts 6 = 1
pts _ = 0
Muito melhor. Faça o teste desta nova implementação e veja que funciona da mesma forma que o método anteiror.
O acontece aqui é o que chamamos de casamento de padrões. Quando a função é chamada com um certo argumento x
, o compilador vai buscar qual dos padrões de argumentos mostrados do lado esquerdo são iguais a x
. Quando encontrar, vai executar a expressão do lado direito:
- Ao executar
pts 3
, o comiplador passará porpts 1
,pts 2
e chegará apts 3
. - Como o argumento de chamada é igual ao argumento do padrão, (
3 == 3
), a expressão do lado direito é executada, e a função retorna4
.
É bastante comum que o último padrão use _
como argumento para indicar qualquer valor, isto é, se o padrão não se encaixar em nenhum caso anterior, então se encaixará no caso em que qualquer valor é válido:
- Ao executar
pts 0
, o comiplador passará porpts 1
,pts 2
,pts 3
,pts 4
,pts 5
,pts 6
e chegará apts _
. - Como
_
indica qualquer valor e0
é um valor qualquer, a expressão do lado direito é executada, e função retorna0
.
A ordem dos padrões é importante para a execução correta da função. Verifique você mesmo o que acontece se o padrão _
for definido entre 2
e 3
, por exemplo. Ignore qualquer mensagem de erro que possa aparecer e tente executar pts 5
.
Perceba que não usamos nenhuma varíavel na definição de pts
, apenas valores. Isso é perfeitamente possível, uma vez que todos os valores retornados são constantes. Entretanto, olhando mais de perto, pts
obedece a seguinte fórmula matemática:
A notação matemática já nos dá uma indicação do que podemos tentar: misturar casamento de padrões e guardas. De fato, Haskell nos permite fazer exatamente isso:
pts :: Int -> Int
pts 1 = 10
pts 2 = 6
pts x
| x <= 6 = 7 - x
| otherwise = 0
Fica fácil perceber que agora, em vez de usarmos _
para capturar qualquer padrão não especificado, temos otherwise
fazendo isso.
- Exercício
A versão de casamento de padrões de pts
e esta última versão mista são ligeiramente diferentes. Você consegue ver a diferença? Conseguiria reescrever a versão mista para que as duas retornem os mesmos resultados sempre? Dica: compare a definição matemática e a implementação em Haskell e preste atenção a condição implícita da definição matemática.
Além de número inteiros, casamento de padrões funciona qualquer tipo de dado. Um bom exemplo é o operador lógico ou, (||)
, que vimos em Verdadeiro ou falso. Ele poderia ser definido como sendo:
(||) :: Bool -> Bool -> Bool
True || _ = True
_ || True = True
_ || _ = False
ou
(||) :: Bool -> Bool -> Bool
True || _ = True
False || y = y
Ou ainda:
(||) :: Bool -> Bool -> Bool
False || False = False
_ || _ = True
Se usarmos casamento de padrões em funções de mais de um argumento, as expressões só serão avaliadas se todos os argumentos se encaixarem em algum padrão. E se os argumentos não se encaixarem em nenhum padrão, teremos um erro de execução. Por isso, como dito anteriormente, é sempre bom ter um caso _
ou otherwise
para capturar padrões inesperados ou indesejados.
Vejamos um exemplo de padrão que não funciona. A função lógica E não poderia ser definida assim:
(&&) :: Bool -> Bool -> Bool
x && x = x
_ && _ = False
Mesmo que usemos x
nos dois argumentos para indicar que eles devem ser iguais, o compilador não compara argumentos entre si. A primeira linha é, portanto, equivalente a segunda. Além disso, ainda teremos um erro do compilador dizendo que x
foi definido múltiplas vezes.
Padrões com n-uplas e listas
editarOs exemplos anteriores mostram que usar casamento de padrões para escrever funções resultam num código muito mais legível e elegante. Entretanto, eles não ilustram por que este método é tão importante. Primeiro, considere que você precisa implementar a função fst
, que extrai o primeiro elemento de uma dupla. Esta parece ser uma tarefa impossível com os conhecimentos básicos de Haskell, já que a única maneira de acessar o primeiro elemento de uma dupla é usando a própria função fst
. Entretanto, veja a função fst'
a seguir, que realiza o mesmo trabalho que fst
:
fst' :: (a, b) -> a
fst' (x, _) = x
Parece mágica, mas é apenas casamento de padrões com n-uplas (uma dupla, neste caso). Em fst'
, se aplicarmos um argumento (1,2)
, por exemplo, o primeiro elemento 1
seria associado a variável x
, enquanto que 2
seria associado a _
e descartado. Depois disso, pode-se fazer qualquer operação com x
do lado direito.
- Exercício
Escreva uma função quarto
que extraia o quarto elemento de uma 10-upla.
Um padrão semelhante pode ser usado em listas. Vejamos a definição de head
e tail
head :: [a] -> a
head (x:_) = x
head [] = error "Prelude.head: empty list"
tail :: [a] -> [a]
tail (_:xs) = xs
tail [] = error "Prelude.tail: empty list"
A lista, assim como uma dupla, é dividia em dois elementos: cabeça e cauda. A cabeça é o elemento que aparece à esquerda do cons ((:)
), e a cauda, o que aparece à direita. No caso de head
e tail
, ainda precisamos de um padrão específico para o caso de uma lista vazia, o qual usa a função error
para exibir uma mensagem de erro durante a execução da aplicação.
Resumindo, a verdadeira vantagem de usar casamento de padrões é o fato de facilitar o acesso a valores contidos em estruturas de dados mais complexas. Casamento de padrões com listas são bastante usados em Haskell, especialmente em funções recursivas, como veremos no capítulo Recurssão.
Associações let
editar
Para concluirmos, uma breve introdução a associações let
, que nada mais são que uma alternativa ao uso de where
, sendo elas intercambiáveis muitas das vezes.
A sintaxe de where
é: <expressões> where <associações>
. No caso de let
, temos o contrário: let <associações> in <expressões>
. Para se lembrar melhor, basta traduzir do inglês: seja <associações> em <expressões>.
Vejamos um problema simples: encontrar as soluções da equação . A soluções são dadas por: . Uma função para calcular a dupla de soluções de poderia ser:
solucao a b c =
( (-b + sqrt(b * b - 4 * a * c)) / (2 * a)
, (-b - sqrt(b * b - 4 * a * c)) / (2 * a) )
Perceba a repetição de sqrt(b * b - 4 * a * c)
. Poderíamos usar where
para definir raizDelta = sqrt(b * b - 4 * a * c)
uma única vez na função:
solucao a b c =
( (-b + raizDelta) / (2 * a)
, (-b - raizDelta) / (2 * a) )
where aizDelta = sqrt(b * b - 4 * a * c)
Ou podemos usar let
:
solucao a b c =
let raizDelta = sqrt(b * b - 4 * a * c)
in ( (-b + raizDelta) / (2 * a)
, (-b - raizDelta) / (2 * a) )
Notas
editar- ↑ Se você já tem experiência com outras linguagens, já deve ter usado
elseif
ou algo parecido. Em Haskell, entretanto, não há esta funcionalidade, portanto precisamos aninhar as expressões de forma explícita.