Давайте рассмотрим тип данных со многими конструкторами:
data T = Alpha Int | Beta Int | Gamma Int Int | Delta Int
Я хочу записать функцию, чтобы проверить, производятся ли два значения с тем же конструктором:
sameK (Alpha _) (Alpha _) = True
sameK (Beta _) (Beta _) = True
sameK (Gamma _ _) (Gamma _ _) = True
sameK _ _ = False
Поддержание sameK
не много забавы, она не может быть проверена на правильность легко. Например, когда новые конструкторы добавляются к T
, легко забыть обновлять sameK
. Я опустил одну строку для предоставления примера:
-- it’s easy to forget:
-- sameK (Delta _) (Delta _) = True
Вопрос состоит в том, как избежать шаблона в sameK
? Или как удостовериться, что это проверяет на все T
конструкторы?
Обходное решение, которое я нашел, должно использовать отдельные типы данных для каждого из конструкторов, происходя Data.Typeable
, и объявляя класс общего типа, но мне не нравится это решение, потому что это намного менее читаемо, и иначе просто простой алгебраический тип работает на меня:
{-# 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
Посмотрите на модуль Data.Data, в частности на функцию toConstr
. Наряду с {- # LANGUAGE DeriveDataTypeable # -}
, вы получите однострочное решение, которое работает для любого типа, который является экземпляром Data.Data
. Вам не нужно разбираться в SYB полностью!
Если по какой-то причине (застряли с Hugs?), Это не вариант, то это очень уродливый и очень медленный способ взлома. Он работает только в том случае, если ваш тип данных Показать
может (например, с помощью извлечения (Показать)
- что означает, например, отсутствие типов функций внутри).
constrT :: T -> String
constrT = head . words . show
sameK x y = constrT x == constrT y
constrT
получает строковое представление внешнего конструктора значения T
, показывая его, разбивая на слова и затем получая первое. Я даю явную сигнатуру типа, чтобы у вас не возникало соблазна использовать ее для других типов (и чтобы обойти ограничение мономорфизма).
Некоторые заметные недостатки:
data T2 = Eta Int | T2: ^: T2
) показом
, такими как многие типы библиотек. Тем не менее, это Haskell 98 ... но это единственное, что я могу сказать по этому поводу!
Для этого вам понадобится библиотека универсальных шаблонов, например Scrap Your Boilerplate или uniplate.
Если вы не хотите быть таким жестким, вы можете использовать решение Дэйва Хинтона вместе с ярлыком пустой записи:
...
where f (Alpha {}) = 0
f (Beta {}) = 1
f (Gamma {}) = 2
Таким образом, вам не нужно знать, сколько аргументов имеет каждый конструктор. Но, очевидно, все же оставляет желать лучшего.
В некоторых случаях может помочь библиотека «Scrap Your Boilerplate».
Другой возможный способ:
sameK x y = f x == f y
where f (Alpha _) = 0
f (Beta _) = 1
f (Gamma _ _) = 2
-- runtime error when Delta value encountered
Ошибка во время выполнения - не идеальный вариант, но лучше, чем молча дать неправильный ответ.
Вы определенно можете использовать дженерики, чтобы избавиться от шаблонов. Ваш код - это хрестоматийный пример того, почему я (и многие другие никогда не используют _
подстановочный знак на верхнем уровне). Хотя расписывать все случаи утомительно, это менее утомительно, чем разбираться с ошибками.
В этом счастливом примере я бы не только использовал решение Дэйва Хинтона, но и наложил бы прагму INLINE на вспомогательную функцию f
.