Haskell/Casamento de padrões, if e let: diferenças entre revisões

Origem: Wikilivros, livros abertos por um mundo aberto.
[edição não verificada][edição não verificada]
Conteúdo apagado Conteúdo adicionado
→‎Padrões com n-uplas e listas: Mudança do exercício
Linha 217: Linha 217:


{{Exercício|1=
{{Exercício|1=
Escreva uma função <code>snd'</code> que extraia o segundo elemento de uma dupla.}}
Escreva uma função <code>quarto</code> que extraia o quarto elemento de uma 10-upla.}}


Um padrão semelhante pode ser usado em listas. Vejamos a definição de <code>head</code> e <code>tail</code>
Um padrão semelhante pode ser usado em listas. Vejamos a definição de <code>head</code> e <code>tail</code>

Revisão das 10h03min de 7 de janeiro de 2018


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

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

É 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

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

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

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

  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.