« La correspondance des modèles » des constructeurs de données de type algébrique
-
26-09-2019 - |
Question
Considérons un type de données avec de nombreux constructeurs:
data T = Alpha Int | Beta Int | Gamma Int Int | Delta Int
Je veux écrire une fonction pour vérifier si deux valeurs sont produits avec le même constructeur:
sameK (Alpha _) (Alpha _) = True
sameK (Beta _) (Beta _) = True
sameK (Gamma _ _) (Gamma _ _) = True
sameK _ _ = False
Le maintien sameK
est pas très amusant, il ne peut pas être vérifié pour l'exactitude facilement. Par exemple, lorsque de nouveaux constructeurs sont ajoutés à T
, il est facile d'oublier de mettre à jour sameK
. J'omis une ligne pour donner un exemple:
-- it’s easy to forget:
-- sameK (Delta _) (Delta _) = True
La question est de savoir comment éviter boilerplate dans sameK
? Ou comment assurez-vous qu'il vérifie pour tous les constructeurs de T
?
La solution que j'ai trouvé est d'utiliser des types de données distincts pour chacun des constructeurs, dérivant Data.Typeable
et déclarant une classe de type commun, mais je ne aime pas cette solution, car il est beaucoup moins lisible et par ailleurs juste un type simple algébrique fonctionne pour moi:
{-# 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
La solution
Regardez le module Data.Data, la fonction toConstr
en particulier. Avec {-# LANGUAGE DeriveDataTypeable #-}
que vous obtiendrez une solution à 1 ligne qui fonctionne pour tout type qui est une instance de Data.Data
. Vous n'avez pas besoin de comprendre tous SYB!
Si, pour une raison quelconque (coincé avec étreintes?), Qui ne sont pas une option, alors voici un hack très laid et très lent. Il ne fonctionne que si votre type de données est Show
able. (Par exemple en utilisant deriving (Show)
- ce qui signifie aucun type de fonction à l'intérieur, par exemple)
constrT :: T -> String
constrT = head . words . show
sameK x y = constrT x == constrT y
constrT
obtient la représentation de chaîne du constructeur le plus externe d'une valeur de T
en le montrant, hacher en mots puis obtenir le premier. Je donne une signature de type explicite de sorte que vous n'êtes pas tenté de l'utiliser sur d'autres types (et de se soustraire à la restriction de monomorphisme).
Quelques inconvénients notables:
- Ce pauses horriblement quand le type a des constructeurs infixes (tels que
data T2 = Eta Int | T2 :^: T2
) - Si certains de vos constructeurs ont un préfixe commun, cela va devenir plus lent, comme une plus grande partie des chaînes doit être comparé.
- ne fonctionne pas sur les types avec un
show
personnalisé, comme de nombreux types de bibliothèques.
Cela dit, il Haskell 98 ... mais qui est sur la seule bonne chose que je peux dire à ce sujet!
Autres conseils
Une autre façon possible:
sameK x y = f x == f y
where f (Alpha _) = 0
f (Beta _) = 1
f (Gamma _ _) = 2
-- runtime error when Delta value encountered
Une erreur d'exécution est pas idéal, mais mieux que de donner en silence la mauvaise réponse.
Vous aurez besoin d'utiliser une bibliothèque de médicaments génériques comme la ferraille Votre boilerplate ou uniplaque à faire en général.
Si vous ne voulez pas être si lourd remis, vous pouvez utiliser la solution de Dave Hinton, en même temps que le raccourci enregistrement vide:
...
where f (Alpha {}) = 0
f (Beta {}) = 1
f (Gamma {}) = 2
Vous ne devez pas savoir combien chaque constructeur a args. Mais il laisse évidemment encore à désirer.
Dans certains cas, "Scrap Votre boilerplate" bibliothèque aide.
Vous pouvez certainement utiliser les médicaments génériques pour éliminer le passe-partout. Votre code est un exemple de manuel pourquoi je (et beaucoup d'autres jamais utiliser le caractère générique _
au niveau supérieur). Alors qu'il est fastidieux d'écrire tous les cas, il est moins pénible que de traiter les bugs.
Dans cet exemple, je serais heureux d'utiliser non seulement la solution de Dave Hinton mais je gifler un pragma INLINE sur la fonction auxiliaire f
.