"Combinação de padrões" de construtores de dados do tipo algébrico
-
26-09-2019 - |
Pergunta
Vamos considerar um tipo de dados com muitos construtores:
data T = Alpha Int | Beta Int | Gamma Int Int | Delta Int
Quero escrever uma função para verificar se dois valores são produzidos com o mesmo construtor:
sameK (Alpha _) (Alpha _) = True
sameK (Beta _) (Beta _) = True
sameK (Gamma _ _) (Gamma _ _) = True
sameK _ _ = False
Manutenção sameK
Não é muito divertido, não pode ser verificado para correção facilmente. Por exemplo, quando novos construtores são adicionados a T
, é fácil esquecer de atualizar sameK
. Eu omiti uma linha para dar um exemplo:
-- it’s easy to forget:
-- sameK (Delta _) (Delta _) = True
A questão é como evitar o boilerplate em sameK
? Ou como garantir que ele verifique para todos T
construtores?
A solução alternativa que encontrei é usar tipos de dados separados para cada um dos construtores, derivando Data.Typeable
, e declarando uma classe de tipo comum, mas não gosto dessa solução, porque é muito menos legível e, de outra forma
{-# LANGUAGE DeriveDataTypeable #-}
import Data.Typeable
class Tlike t where
value :: t -> t
value = id
data Alpha = Alpha Int deriving Typeable
data Beta = Beta Int deriving Typeable
data Gamma = Gamma Int Int deriving Typeable
data Delta = Delta Int deriving Typeable
instance Tlike Alpha
instance Tlike Beta
instance Tlike Gamma
instance Tlike Delta
sameK :: (Tlike t, Typeable t, Tlike t', Typeable t') => t -> t' -> Bool
sameK a b = typeOf a == typeOf b
Solução
Veja o módulo Data.data, o toConstr
função em particular. Juntamente com {-# LANGUAGE DeriveDataTypeable #-}
Isso lhe dará uma solução de 1 linha que funciona para qualquer tipo que seja uma instância de Data.Data
. Você não precisa descobrir todo o Syb!
Se, por algum motivo (preso com abraços?), Isso não é uma opção, aqui está um hack muito feio e muito lento. Funciona apenas se o seu tipo de dados for Show
capaz (por exemplo, usando deriving (Show)
- o que significa nenhum tipo de função dentro, por exemplo).
constrT :: T -> String
constrT = head . words . show
sameK x y = constrT x == constrT y
constrT
recebe a representação da string do construtor mais externo de um T
valor mostrando -o, cortando -o em palavras e depois recebendo o primeiro. Eu dou uma assinatura de tipo explícita para que você não seja tentado a usá -la em outros tipos (e a fugir da restrição de monomorfismo).
Algumas desvantagens notáveis:
- Isso quebra terrivelmente quando seu tipo tem construtores de infix (como
data T2 = Eta Int | T2 :^: T2
) - Se alguns de seus construtores tiverem um prefixo compartilhado, isso será mais lento, pois uma parte maior das cordas deve ser comparada.
- Não funciona em tipos com um costume
show
, como muitos tipos de biblioteca.
Dito isto, isso é Haskell 98 ... mas isso é a única coisa boa que posso dizer sobre isso!
Outras dicas
Outra maneira possível:
sameK x y = f x == f y
where f (Alpha _) = 0
f (Beta _) = 1
f (Gamma _ _) = 2
-- runtime error when Delta value encountered
Um erro de tempo de execução não é ideal, mas melhor do que dar a resposta errada silenciosamente.
Você precisará usar uma biblioteca de genéricos, como sucata o seu caldeira ou uniplate para fazer isso em geral.
Se você não quer ser tão pesado, pode usar a solução de Dave Hinton, juntamente com o atalho de registro vazio:
...
where f (Alpha {}) = 0
f (Beta {}) = 1
f (Gamma {}) = 2
Então você não precisa saber quantos args cada construtor possui. Mas obviamente ainda deixa algo a desejar.
Em alguns casos, a biblioteca "raspa sua caldeira" ajudará.
Definitivamente, você pode usar os genéricos para eliminar o caldeira. Seu código é um exemplo de livro didático por que eu (e muitos outros Nunca use o _
curinga no nível superior). Embora seja tedioso escrever todos os casos, é menos tedioso do que lidar com os bugs.
Neste exemplo feliz, eu não apenas usaria a solução de Dave Hinton, mas também daria um tapa em um Pragma em linha na função auxiliar f
.