Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Better Way to Define an Enum in Haskell

Tags:

enums

haskell

People also ask

How do you declare an enum?

An enum is defined using the enum keyword, directly inside a namespace, class, or structure. All the constant names can be declared inside the curly brackets and separated by a comma. The following defines an enum for the weekdays. Above, the WeekDays enum declares members in each line separated by a comma.

Can an enum have one value?

With an enum type with only one value, most developers will not expect that (UserIDEnum)42 is used, since it's not a "defined" value of the enum.

How do you know if a class is enum?

In C#, we can check the specific type is enum or not by using the IsEnum property of the Type class. It will return true if the type is enum. Otherwise, this property will return false.

How do I find my enum instance?

if (obj. getClass(). isEnum()) { ... } If Enum is your custom class, then just check that obj instanceof Enum .


data MyDataType = Foo | Bar | Baz deriving (Enum)

instance Enum MyDataType where
    fromEnum = fromJust . flip lookup table
    toEnum = fromJust . flip lookup (map swap table)
table = [(Foo, 0), (Bar, 1), (Baz, 2)]

The problem with the accepted solution is the compiler won't tell you when you are missing an enum in your table. The deriving Enum solution is great, but it won't work if you want to have an arbitrary mapping to numbers. Another answer suggests Generics or Template Haskell. This follows up on that by using 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

We will get compiler warning in the toNumber case mapping when a new constructor is added.

Now we just need the ability to turn that code into data so that the mapping can be automatically reversed. Here we generate the same table mentioned in the accepted solution.

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

You can fill out an Enum class just the same as in the accepted answer. Unmentioned there is that you can also fill out the Bounded class.


Since you say the numbers are not generated by any regular law, you could use generic programming (e.g. with Scrap Your Boilerplate) or Template Haskell to implement a generic solution to this problem. I tend to prefer Template Haskell because it actually generates code and compiles it, so you get all the type-checking and optimisation benefits of GHC.

I wouldn't be surprised if someone had implemented this already. It should be trivial.


My examples here are using GHCI 8.4.4 with a prompt, "λ: ".

I think deriving from Enum makes the most sense here, as the most fundamental types in Haskell also derive from Enum (tuples, characters, integers, etc...), and it has builtin methods of getting values into and from the enum.

First, create a data type deriving Enum (and Show so you can view the value in the REPL and Eq to enable .. range completion):

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

Enums define a method, fromEnum, which you can use to get the values as requested in the question (0, 1, and 2).

Usage:

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

It is a simple matter to define a function giving an arbitrary value (such as powers of two using the integer power operator, ^):

λ: value e = 2 ^ (fromEnum e)

Usage:

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

Another answer says:

The deriving Enum solution is great, but it won't work if you want to have an arbitrary mapping to numbers.

Well, let's see about that (use :set +m to enable multiline input in GHCI, if you haven't already):

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

Usage:

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

We just demonstrated that it does indeed work, but I would prefer to calculate it from the fromEnum as we did with value, if we do not want values increasing by 1 from 0.