Лучший способ определить перечисление в Haskell - PullRequest
41 голосов
/ 14 мая 2011

Я хочу, чтобы тип данных представлял собой конечный набор целых чисел, к которым могут обращаться конкретные имена. Я считаю, что лучший способ сделать это - использовать Enum.

Однако есть одна небольшая проблема. Единственный известный мне способ определения Enum - это что-то вроде этого:

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 

Обратите внимание, что мне приходится повторять одну и ту же пару два раза - один раз при определении отображения целого числа в перечисление и другой раз при определении отображения перечисления в целое число.

Есть ли способ избежать этого повторения?

Ответы [ 5 ]

50 голосов
/ 14 мая 2011
data MyDataType = Foo | Bar | Baz deriving (Enum)
32 голосов
/ 14 мая 2011
instance Enum MyDataType where
    fromEnum = fromJust . flip lookup table
    toEnum = fromJust . flip lookup (map swap table)
table = [(Foo, 0), (Bar, 1), (Baz, 2)]
13 голосов
/ 01 марта 2015

Проблема с принятым решением заключается в том, что компилятор не сообщит вам, когда вы пропустите перечисление в вашей таблице. Решение deriving Enum отлично, но оно не будет работать, если вы хотите иметь произвольное отображение на числа. Другой ответ предлагает Generics или Template Haskell. Это следует с помощью 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

Мы получим предупреждение компилятора в случае отображения toNumber при добавлении нового конструктора.

Теперь нам просто нужна возможность преобразовать этот код в данные, чтобы сопоставление можно было автоматически обратить вспять. Здесь мы генерируем то же самое table, которое указано в принятом решении.

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

Вы можете заполнить класс Enum точно так же, как в принятом ответе. Не упомянуто, что вы также можете заполнить класс Bounded.

3 голосов
/ 14 мая 2011

Поскольку вы говорите, что числа не генерируются никаким обычным законом, вы можете использовать общее программирование (например, с Scrap Your Boilerplate) или Template Haskell для реализации общего решения этой проблемы.Я предпочитаю Template Haskell, потому что он на самом деле генерирует код и компилирует его, поэтому вы получаете все преимущества GHC для проверки типов и оптимизации.

Я не удивлюсь, если кто-то уже реализовал это.Это должно быть тривиально.

2 голосов
/ 30 декабря 2018

Мои примеры здесь используют GHCI 8.4.4 с приглашением, "λ: ".

Я думаю, что вывод из Enum имеет наибольшее значение, поскольку наиболее фундаментальные типы в Haskell также происходят от Enum (кортежи, символы, целые числа и т. Д.), И он имеет встроенные методы для получения значений в и из перечисления.

Сначала создайте тип данных, производный EnumShow, чтобы вы могли просмотреть значение в REPL и Eq для включения .. завершения диапазона):

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

Перечисления определяют метод, fromEnum, который можно использовать для получения значений, запрошенных в вопросе (0, 1 и 2).

Использование:

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

Этопростое определение функции, дающей произвольное значение (например, степени двойки с использованием целочисленного оператора мощности, ^):

λ: value e = 2 ^ (fromEnum e)

Использование:

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

Другой ответ говорит:

Решение deriving Enum отлично, но оно не будет работать, если вы хотите иметь произвольное отображение на числа.

Хорошо, давайте посмотрим на это (используйте :set +m, чтобы включить многострочный ввод в GHCI, если вы этого еще не сделали):

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

Использование:

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

Мытолько что продемонстрировал, что он действительно работает, но я бы предпочел рассчитать его по fromEnum, как мы это делали с value, если мы не хотим, чтобы значения увеличивались на 1 с 0.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...