Em Haskell, por que os padrões não exaustivos não são erros no tempo de compilação?
Pergunta
Este é um acompanhamento de Por que estou recebendo "padrões não exaustivos em função ..." quando invoco minha função de substring Haskell?
Eu entendo isso usando -Wall
, O GHC pode alertar contra padrões não exaustivos. Estou me perguntando qual é a razão por não torná-lo um erro de tempo de compilação por padrão, uma vez que é sempre possível definir explicitamente uma função parcial:
f :: [a] -> [b] -> Int
f [] _ = error "undefined for empty array"
f _ [] = error "undefined for empty array"
f (_:xs) (_:ys) = length xs + length ys
A questão não é específica do GHC.
É porque ...
- Ninguém queria aplicar um compilador Haskell para executar esse tipo de análise?
- Uma pesquisa de padrões não exaustiva pode encontrar alguns, mas não todos os casos?
- As funções parcialmente definidas são consideradas legítimas e usadas com frequência suficiente para não impor o tipo de construto mostrado acima? Se for esse o caso, você pode me explicar por que os padrões não exaustivos são úteis/legítimos?
Solução
Há casos em que você não se importa que uma correspondência de padrões seja não exaustiva. Por exemplo, embora essa possa não ser a implementação ideal, acho que não ajudaria se não compilar:
fac 0 = 1
fac n | n > 0 = n * fac (n-1)
Que isso não é exaustivo (números negativos não correspondem a nenhum caso) não importa realmente para o uso típico da função fatorial.
Além disso, geralmente não é possível decidir para o compilador se uma correspondência de padrão for exaustiva:
mod2 :: Integer -> Integer
mod2 n | even n = 0
mod2 n | odd n = 1
Aqui todos os casos devem ser cobertos, mas o compilador provavelmente não pode detectá -lo. Como os guardas podem ser arbitrariamente complexos, o compilador nem sempre pode decidir se os padrões são exaustivos. Claro que este exemplo seria melhor escrito com otherwise
, mas acho que também deve compilar em sua forma atual.
Outras dicas
Você pode usar -Werror
para transformar avisos em erros. Não sei se você pode transformar apenas os padrões não exaustivos em erros, desculpe!
Quanto à terceira parte da sua pergunta:
Às vezes, escrevo uma série de funções que tendem a trabalhar juntos e tenho propriedades que você não pode expressar facilmente em Haskell. Pelo menos algumas dessas funções tendem a ter padrões não exaustivos, geralmente os 'consumidores'. Isso surge, por exemplo, em funções que são 'meio de' inversas umas das outras.
Um exemplo de brinquedo:
duplicate :: [a] -> [a]
duplicate [] = []
duplicate (x:xs) = x : x : (duplicate xs)
removeDuplicates :: Eq a => [a] -> [a]
removeDuplicates [] = []
removeDuplicates (x:y:xs) | x == y = x : removeDuplicates xs
Agora é muito fácil ver isso removeDuplicates (duplicate as)
é igual a as
(Sempre que o tipo de elemento está em Eq
), mas em geral duplicate (removeDuplicates bs)
vai travar, porque há um número ímpar de elementos ou 2 elementos consecutivos diferem. Se não trava, é porque bs
foi produzido por (ou poderia ter sido produzido por) duplicate
em primeiro lugar!.
Portanto, temos as seguintes leis (não válidas Haskell):
removeDuplicates . duplicate == id
duplicate . removeDuplicates == id (for values in the range of duplicate)
Agora, se você quiser evitar padrões não exaustivos aqui, você pode fazer removeDuplicates
Retorna Maybe [a]
, ou adicione mensagens de erro para os casos ausentes. Você poderia até fazer algo parecido
newtype DuplicatedList a = DuplicatedList [a]
duplicate :: [a] -> DuplicatedList a
removeDuplicates :: Eq a => DuplicatedList a -> [a]
-- implementations omitted
Tudo isso é necessário, porque você não pode expressar facilmente 'ser uma lista de comprimento uniforme, com pares consecutivos de elementos sendo iguais' no sistema do tipo Haskell (a menos que você seja Oleg :)
Mas se você não exportar removeDuplicates
Eu acho que é perfeitamente bom usar padrões não exaustivos aqui. Assim que você o exportar, você perderá o controle sobre as entradas e terá que lidar com os casos ausentes!