web-dev-qa-db-fra.com

Meilleure façon de définir un enum dans Haskell

Je souhaite qu'un type de données représente un ensemble fini d'entiers pouvant être traités par des noms spécifiques. Je pense que la meilleure façon de faire est d’utiliser un Enum. 

Cependant, il y a un petit problème. Le seul moyen que je connaisse pour définir un Enum est quelque chose comme ceci:

data MyDataType = Foo | Bar | Baz

instance Enum MyDataType 
 toEnum 0 = Foo
 toEnum 1 = Bar
 toEnum 2 = Baz

 fromEnum Foo = 0
 fromEnum Bar = 1
 fromEnum Baz = 2 

Notez que je dois répéter la même paire deux fois - une fois lorsque vous définissez un mappage d’entier à enum et une fois lorsque vous définissez un mappage d’énum à entier. 

Y a-t-il un moyen d'éviter cette répétition?

37
user500944
instance Enum MyDataType where
    fromEnum = fromJust . flip lookup table
    toEnum = fromJust . flip lookup (map swap table)
table = [(Foo, 0), (Bar, 1), (Baz, 2)]
31
augustss
data MyDataType = Foo | Bar | Baz deriving (Enum)
49
Robin Green

Le problème avec la solution acceptée est que le compilateur ne vous dira pas s'il vous manque une énumération dans votre table. La solution deriving Enum est excellente, mais cela ne fonctionnera pas si vous souhaitez un mappage arbitraire sur des nombres. Une autre réponse suggère Generics ou Template Haskell. Ceci fait suite à cela en utilisant Data.

{-# Language DeriveDataTypeable #-}
import Data.Data
data MyDataType = Foo | Bar | Baz deriving (Eq, Show, Data, Typeable)

toNumber enum = case enum of
   Foo -> 1
   Bar -> 2
   Baz -> 4

Nous aurons un avertissement du compilateur dans le mappage de cas toNumber quand un nouveau constructeur sera ajouté.

Maintenant, nous avons juste besoin de pouvoir transformer ce code en données afin que le mappage puisse être automatiquement inversé. Ici, nous générons la même variable table mentionnée dans la solution acceptée.

table = map (\cData -> let c = (fromConstr cData :: MyDataType) in (c, toNumber c) )
      $ dataTypeConstrs $ dataTypeOf Foo

Vous pouvez remplir une classe Enum de la même manière que dans la réponse acceptée. Sans le mentionner, vous pouvez également renseigner la classe Bounded.

12
Greg Weber

Puisque vous dites que les chiffres ne sont pas générés par une loi en vigueur, vous pouvez utiliser une programmation générique (par exemple avec Scrap Your Boilerplate) ou un modèle Haskell pour implémenter une solution générique à ce problème. J'ai tendance à préférer Template Haskell car il génère du code et le compile, ce qui vous permet de bénéficier de tous les avantages de la vérification de type et de l'optimisation de GHC.

Je ne serais pas surpris si quelqu'un avait déjà implémenté cela. Cela devrait être trivial.

3
Robin Green

Mes exemples ici utilisent GHCI 8.4.4 avec une invite, "λ: ".

Je pense que dériver de Enum a plus de sens ici, car les types les plus fondamentaux en Haskell dérivent également de Enum (n-uplets, caractères, nombres entiers, etc.), et il a des méthodes intégrées pour obtenir des valeurs dans et à partir de l'énum.

Commencez par créer un type de données dérivé de Enum (et Show afin de pouvoir afficher la valeur dans le REPL et Eq pour activer la complétion de la plage ..):

λ: data MyDataType = Foo | Bar | Baz deriving (Enum, Show, Eq)
λ: [Foo ..]
[Foo,Bar,Baz]

Les énumérations définissent une méthode, fromEnum , que vous pouvez utiliser pour obtenir les valeurs demandées dans la question (0, 1 et 2).

Usage:

λ: map fromEnum [Foo ..]
[0,1,2]

Il est simple de définir une fonction donnant une valeur arbitraire (telle que des puissances de deux utilisant l'opérateur de puissance entier, ^):

λ: value e = 2 ^ (fromEnum e)

Usage:

λ: map value [Foo ..]
[1,2,4]

Une autre réponse dit:

La solution deriving Enum est excellente, mais cela ne fonctionnera pas si vous souhaitez un mappage arbitraire sur des nombres.

Eh bien, voyons à ce sujet (utilisez :set +m pour activer la saisie multiligne dans GHCI, si ce n'est déjà fait):

arbitrary e = case e of
  Foo -> 10
  Bar -> 200
  Baz -> 3000

Usage:

λ: map arbitrary [Foo ..]
[10,200,3000]

Nous venons de démontrer que cela fonctionnait bien, mais je préférerais le calculer à partir de la variable fromEnum comme nous l'avons fait avec value, si nous ne voulons pas que les valeurs augmentent de 1 à 0.

1
Aaron Hall