Haskell/Casamento de padrões, if e let

Origem: Wikilivros, livros abertos por um mundo aberto.


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:

  1. A condição avaliada é x < 0.
  2. Se a condição retornar um Bool True, a expressão dentro de then é avaliada: a função minhaSignum retornará -1
  3. Se a condição retornar um Bool False, a expressão dentro de else é avaliada: uma nova expressão if then else deve ser avaliada.
    1. A condição avaliada é x > 0.
    2. Se a condição retornar um Bool True, a expressão dentro de then é avaliada: a função minhaSignum retornará 1;
    3. Se a condição retornar um Bool False, a expressão dentro de else é avaliada: a função minhaSignum retornará 0.

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:

  1. As condições avaliadas por if devem sempre retornar um Bool.
  2. Os valores finais das expressões dentro de then e else, 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:

  1. Ao executar pts 3, o comiplador passará por pts 1, pts 2 e chegará a pts 3.
  2. Como o argumento de chamada é igual ao argumento do padrão, (3 == 3), a expressão do lado direito é executada, e a função retorna 4.

É 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:

  1. Ao executar pts 0, o comiplador passará por pts 1, pts 2, pts 3, pts 4, pts 5, pts 6 e chegará a pts _.
  2. Como _ indica qualquer valor e 0 é um valor qualquer, a expressão do lado direito é executada, e função retorna 0.

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) )
Observações: Fique atento à indentação! Haskell é sensível a indentação do código, e é sempre recomendável usar espaços em branco em vez de tabulação. Em Haskell usam-se dois ou quatros espaços. Se você quiser insistir em usar tabulação, assegure-se de que todas tenham o mesmo comprimento. Falaremos mais sobre este assunto em Indentação.

Notas[editar | editar código-fonte]

  1. 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.