Maratona de Haskell - Oitavo Dia

Fala pessoal!


Hoje vamos falar de Pattern Matching uma das melhores funcionalidades em Functional Programming na minha opinião.

Como a maioria das linguagens funcionais Haskell tem Pattern Matching que faz nossas vidas muito mais fácil apra criar funções com diferentes ações dependendo do que é recebido. Pattern Matching é sobre avaliar dados e desconstrui-los de acordo com seus padrões. A ideia atrás de Pattern Matching é fazer nossos programas mais simples e legíveis. Vamos mergulhar em alguns exemplos e entender o porque.

Nós podemos iniciar com uma função que recebe um número e se este número for 5 então retornamos o mês de “Maio” e se o número é diferente retorna uma mensagem “it is not a valid month”. Como nós implementamos isso como uma linguagem que estamos acostumados?

month :: Int -> String
month a =
  if a == 5 then
    "May"
  else
    "it is not a valid month"


month 5
May
month 6
"it is not a valid month"

Ok. Um simples IF pode resolver o problema, mas e se nós queremos obter “June”, “July” e “August”?

month :: Int -> String
month a =
  if a == 5 then
    "May"
  else if a == 6 then
    "June"
  else if a == 7 then
    "July"
  else if a == 8 then
    "August"
  else
    "it is not a valid month"


month 6
"June"
month 7
"July"
month 8
August
month 9
"it is not a valid month"

A solução do IF continua funcionando, mas é muito feia. Imagine se a função retornar todos os meses.
Quando usamos Pattern Matching em uma função podemos criar um corpo para cada padrão fazendo a função mais simples e legível. Vamos ver:

month' :: Int -> String
month' 5 = "May"
month' 6 = "June"
month' 7 = "July"
month' 8 = "August"


month 5
"May"
month 6
"June"
month 7
"July"
month 8
August
month 9
"*** Exception: code/patter_matching.hs:(17,1)-(20,19): Non-exhaustive patterns in function month'

Muito melhor que a versão do IF. O que você acha?
Preste atenção que cada mês é um novo corpo de função.

Ok. Mas onde esta o ELSE final para os meses que não existem?
Bem, Pattern Matching tem uma funcionalidade chamada catchall que é o mesmo que o ELSE final. No exemplo acima nós vimos a função falhando porque nós não temos o catchall.

month' :: Int -> String
month' 5 = "May"
month' 6 = "June"
month' 7 = "July"
month' 8 = "August"
month' _ = "it is not a valid month"


month 9
"it is not a valid month"

Aqui para o catcall nós estamos usando o underscore (_) mas nós podemos usar qualquer outra letra, simbolo ou palavra. Underscore é somente um padrão do Haskell.

Outro importante ponto é a ordem das funções. Se nós temos as funções em uma ordem incorreta é possível termos um bug. No exemplo dos meses se o catchall esta antes de um mês, este mês nunca será chamado porque o catchall pega todos os casos.

month' :: Int -> String
month' 5 = "May"
month' _ = "it is not a valid month"
month' 6 = "June"
month' 7 = "July"
month' 8 = "August"


*Main> :l code/patter_matching.hs
[1 of 1] Compiling Main             ( code/patter_matching.hs, interpreted )


code/patter_matching.hs:17:1: Warning:
    Pattern match(es) are overlapped
    In an equation for month':
        month' 6 = ...
        month' 7 = ...
        month' 8 = ...
Ok, modules loaded: Main.


month' 5
May
month' 6
"it is not a valid month"
month' 9
"it is not a valid month"

Nós podemos usar o Pattern Matching com Tuples também. Se nós queremos somar duas Tuples como podemos fazer?

points :: (Int, Int) -> (Int, Int) -> (Int, Int)
points (a1, b1) (a2, b2) = (a1 + a2, b1 + b2)


points (2, 5) (7, 1)
(9,6)

Neste caso nós não teremos o catchall porque o primeiro padrão pega todos os casos.
Outro exemplo com Tuples pode ser criar funções para trios. Nós já temos as funções fst e snd para pares, mas nós não temos nenhuma função para trios. Nós podemos criar então:

first :: (a, b, c) -> a
first (a, _, _) = a


second :: (a, b, c) -> b
second (_, b, _) = b


third :: (a, b, c) -> c
third (_, _, c) = c


first (1,2,3)
1
second (1,2,3)
2
third (1,2,3)
3


E o mais importante, nós podemos usar Pattern Matching com listas que é provavelmente onde iremos usar mais esta técnica. Nós vimos anteriormente as funções head e tail que podem ser usadas com listas. Vamos reescreve-las:

head' :: [a] -> a
head' (x:_) = x


tail' :: [a] -> [a]
tail' (_:xs) = xs


head' [1,2,3]
1
tail' [1,2,3]
[2,3]


head' []
*** Exception: code/patter_matching.hs:39:1-15: Non-exhaustive patterns in function head'


tail' []
*** Exception: code/patter_matching.hs:42:1-17: Non-exhaustive patterns in function tail'

Aqui podemos ver que as funções estão funcionando quando recebemos uma lista com elementos, mas quando a lista esta vazia as funções quebram. Vamos resolver isso.

head' :: [a] -> a
head' [] = error "list is empty"
head' (x:_) = x


tail' :: [a] -> [a]
tail' [] = [


head' [1,2,3]
1


tail' [1,2,3]
[2,3]


head' []
*** Exception: list is empty


tail' []
[]

Agora esta tudo funcionando, nós demos duas diferentes soluções para cada função. Para head estamos chamando um erro quando receboms uma lista vazia porque não temos um elemento para retornar, por outro lado para a função tail nós estamos retornando uma lista vazia quando recebemos uma lista vazia porque o resto de nada é nada.

Além disso temos uma curiosidade nessa implementação, nós podemos ver que na função head nós recebemos a lista como (x:_) e na função tail como (_:xs). Isso acontece porque em Haskell nós podemos representar listas com x:xs onde x é o primeiro elemento e xs é o resto.
Um exemplo pode ser a lista [1,2,3,4] que pode ser representada como (1:(2:(3:(4)))). Neste caso 1 é o x e (2:(3:(4))) é o xs.


Pattern Matching é uma funcionalidade especial na programação funcional porque faz o código mais simples e legível. Eu posso dizer que é a minha funcionalidade favorita. Prepare-se porque você vai trabalhar bastante com Pattern Matching quando você estiver usando Functional Programming.

Written on December 8, 2016