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 | editar código-fonte]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 | editar código-fonte]É 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
[editar | editar código-fonte]Considere 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
[editar | editar código-fonte]Os 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 | editar código-fonte]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 | editar código-fonte]- ↑ 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.