Проверьте, соответствует ли значение конструктору
Скажем, у меня есть такой тип данных:
data NumCol = Empty |
Single Int |
Pair Int Int |
Lots [Int]
Теперь я хочу отфильтровать элементы, соответствующие данному конструктору, от [NumCol]
. Я могу написать это, скажем, Pair
:
get_pairs :: [NumCol] -> [NumCol]
get_pairs = filter is_pair
where is_pair (Pair _ _) = True
is_pair _ = False
Это работает, но не является общим. Мне нужно написать отдельную функцию для is_single
, is_lots
и т.д.
Вместо этого я могу написать:
get_pairs = filter (== Pair)
Но это работает только для конструкторов типов, которые не принимают аргументов (т.е. Empty
).
Итак, вопрос в том, как я могу написать функцию, которая принимает значение и конструктор, и возвращает, соответствует ли значение конструктору?
Ответы
Ответ 1
Не менее get_pairs
можно определить относительно просто, используя вместо этого определение списка:
get_pairs xs = [x | [email protected] {} <- xs]
Для более общего решения конструкторов сопоставления вы можете использовать призмы из пакета lens
:
{-# LANGUAGE TemplateHaskell #-}
import Control.Lens
import Control.Lens.Extras (is)
data NumCol = Empty |
Single Int |
Pair Int Int |
Lots [Int]
-- Uses Template Haskell to create the Prisms _Empty, _Single, _Pair and _Lots
-- corresponding to your constructors
makePrisms ''NumCol
get_pairs :: [NumCol] -> [NumCol]
get_pairs = filter (is _Pair)
Ответ 2
Метки помеченных объединений должны быть первоклассными значениями и с небольшим усилием, они есть.
Предупреждение о стрельбе:
{-# LANGUAGE GADTs, DataKinds, KindSignatures,
TypeFamilies, PolyKinds, FlexibleInstances,
PatternSynonyms
#-}
Шаг первый: определите версии тегов на уровне типов.
data TagType = EmptyTag | SingleTag | PairTag | LotsTag
Шаг второй: определить свидетелей уровня ценности для представления тэгов уровня. Библиотека Ричарда Эйзенберга Singletons сделает это за вас. Я имею в виду что-то вроде этого:
data Tag :: TagType -> * where
EmptyT :: Tag EmptyTag
SingleT :: Tag SingleTag
PairT :: Tag PairTag
LotsT :: Tag LotsTag
И теперь мы можем сказать, какие вещи мы ожидаем найти, связанные с данным тегом.
type family Stuff (t :: TagType) :: * where
Stuff EmptyTag = ()
Stuff SingleTag = Int
Stuff PairTag = (Int, Int)
Stuff LotsTag = [Int]
Итак, мы можем реорганизовать тип, который вы впервые подумали о
data NumCol :: * where
(:&) :: Tag t -> Stuff t -> NumCol
и используйте PatternSynonyms
, чтобы восстановить поведение, которое вы имели в виду:
pattern Empty = EmptyT :& ()
pattern Single i = SingleT :& i
pattern Pair i j = PairT :& (i, j)
pattern Lots is = LotsT :& is
Так получилось, что каждый конструктор для NumCol
превратился в тег, индексированный по типу тега. То есть, теги конструктора теперь живут отдельно от остальных данных, синхронизируются общим индексом, который гарантирует, что материал, связанный с тегом, соответствует самому тегу.
Но мы можем говорить только о тегах.
data Ex :: (k -> *) -> * where -- wish I could say newtype here
Witness :: p x -> Ex p
Теперь Ex Tag
- это тип "тегов времени выполнения с аналогией типа". Он имеет экземпляр Eq
instance Eq (Ex Tag) where
Witness EmptyT == Witness EmptyT = True
Witness SingleT == Witness SingleT = True
Witness PairT == Witness PairT = True
Witness LotsT == Witness LotsT = True
_ == _ = False
Кроме того, мы можем легко извлечь тег a NumCol
.
numColTag :: NumCol -> Ex Tag
numColTag (n :& _) = Witness n
И это позволяет нам соответствовать вашей спецификации.
filter ((Witness PairT ==) . numColTag) :: [NumCol] -> [NumCol]
Возникает вопрос, действительно ли ваша спецификация вам нужна. Дело в том, что обнаружение тега дает вам представление об этом материале тега. Тип вывода [NumCol]
не оправдывает того факта, что вы знаете, что у вас есть только пары.
Как вы можете затянуть тип своей функции и доставить ее?
Ответ 3
Один подход заключается в использовании модуля DataTypeable
и Data.Data
. Этот подход основан на двух экземплярах автоматически генерируемых экземпляров типов, которые содержат метаданные о типе для вас: Typeable
и Data
. Вы можете получить их с помощью {-# LANGUAGE DeriveDataTypeable #-}
:
data NumCol = Empty |
Single Int |
Pair Int Int |
Lots [Int] deriving (Typeable, Data)
Теперь мы имеем функцию toConstr
, которая, учитывая значение, дает нам представление своего конструктора:
toConstr :: Data a => a -> Constr
Это упрощает сравнение двух терминов только по их конструкторам. Единственная оставшаяся проблема в том, что нам нужно сравнить значение, когда мы определяем наш предикат! Мы всегда можем просто создать фиктивное значение с помощью undefined
, но это немного уродливо:
is_pair x = toConstr x == toConstr (Pair undefined undefined)
Итак, последнее, что мы сделаем, это определить удобный маленький класс, который автоматизирует это. Основная идея состоит в том, чтобы называть toConstr
по нефункциональным значениям и возвращать на любые функции, сначала передавая в undefined
.
class Constrable a where
constr :: a -> Constr
instance Data a => Constrable a where
constr = toConstr
instance Constrable a => Constrable (b -> a) where
constr f = constr (f undefined)
Это зависит от FlexibleInstance
, OverlappingInstances
и UndecidableInstances
, поэтому он может быть немного злым, но, используя (в) знаменитую теорему о глазном яблоке, все должно быть хорошо. Если вы не добавите больше экземпляров или не попытаетесь использовать его с чем-то, что не является конструктором. Тогда это может взорваться. Насильно. Нет promises.
Наконец, со злым, аккуратно содержащимся, мы можем написать оператор "equal by constructor":
(=|=) :: (Data a, Constrable b) => a -> b -> Bool
e =|= c = toConstr e == constr c
(Оператор =|=
является немного мнемоническим, потому что конструкторы синтаксически определены с помощью |
.)
Теперь вы можете написать почти то, что вам нужно!
filter (=|= Pair)
Кроме того, возможно, вы захотите отключить ограничение мономорфизма. Фактически, здесь список расширений, которые я включил, можно использовать:
{-# LANGUAGE DeriveDataTypeable, FlexibleInstances, NoMonomorphismRestriction, OverlappingInstances, UndecidableInstances #-}
Да, это много. Но это то, что я готов жертвовать ради дела. Не писать дополнительные undefined
s.
Честно говоря, если вы не возражаете полагаться на lens
(но мальчик - это зависимость от doozy), вы должны просто пойти с призрачным подходом. Единственное, что мне рекомендовать, это то, что вы можете использовать класс с названием Data.Data.Data
.