2017-01-07 3 views
3

Предположим, что у меня есть некоторые простые алгебраические данные (по существу перечисления) и другой тип, который имеет эти перечисления как поля.Haskell: есть ли способ «отображения» над алгебраическим типом данных?

data Color = Red | Green | Blue deriving (Eq, Show, Enum, Ord) 
data Width = Thin | Normal | Fat deriving (Eq, Show, Enum, Ord) 
data Height = Short | Medium | Tall deriving (Eq, Show, Enum, Ord) 

data Object = Object { color :: Colour 
        , width :: Width 
        , height :: Height } deriving (Show) 

Учитывая список объектов, я хочу проверить, что все атрибуты различны. Для этого у меня есть следующие функции (с помощью sort из Data.List)

allDifferent = comparePairwise . sort 
    where comparePairwise xs = and $ zipWith (/=) xs (drop 1 xs) 

uniqueAttributes :: [Object] -> Bool 
uniqueAttributes objects = all [ allDifferent $ map color objects 
           , allDifferent $ map width objects 
           , allDifferent $ map height objects ] 

Это работает, но это скорее неудовлетворительное, потому что я должен был напечатать каждое поле (цвет, ширина, высота) вручную. В моем фактическом коде есть больше полей! Есть ли способ «карты» функция

\field -> allDifferent $ map field objects 

над полями алгебраических типов данных, как Object? Я хочу, чтобы рассматривать Object как список его полей (то, что будет легко, например, в JavaScript), но эти поля имеют разные типы ...

+0

Можно использовать лом-ваш-шаблон. Для этого простого случая я не уверен, что это намного лучше. – chi

+1

Вы можете немного его отличить от дженериков: 'uniqueAttributes objects = и [go color, go width, go height], где go :: (Ord a) => (Object -> a) -> Bool; go f = allDifferent (объекты map f) ' –

ответ

4

Вот решение, использующее generics-sop:

pointwiseAllDifferent 
    :: (Generic a, Code a ~ '[ xs ], All Ord xs) => [a] -> Bool 
pointwiseAllDifferent = 
    and 
    . hcollapse 
    . hcmap (Proxy :: Proxy Ord) (K . allDifferent) 
    . hunzip 
    . map (unZ . unSOP . from) 

hunzip :: SListI xs => [NP I xs] -> NP [] xs 
hunzip = foldr (hzipWith ((:) . unI)) (hpure []) 

Этот предполагается, что тип Object вы хотите сравнить это тип записи и требует, чтобы вы сделать этот тип экземпляром класса Generic, которые можно сделать с помощью шаблона Haskell:

deriveGeneric ''Object 

Давайте посмотрим, что это здесь происходит, глядя на конкретном примере:

objects = [Object Red Thin Short, Object Green Fat Short] 

Линия map (unZ . unSOP . from) преобразует каждый Object в гетерогенной список (называемый п-арной продукт в библиотеке):

GHCi> map (unZ . unSOP . from) objects 
[I Red :* (I Thin :* (I Short :* Nil)),I Green :* (I Fat :* (I Short :* Nil))] 

В hunzip затем поворачивает этот список продуктов в продукт, где каждый элемент списка:

GHCi> hunzip it 
[Red,Green] :* ([Thin,Fat] :* ([Short,Short] :* Nil)) 

Теперь мы применяем allDifferent для каждого списка в продукте:

GHCi> hcmap (Proxy :: Proxy Ord) (K . allDifferent) it 
K True :* (K True :* (K False :* Nil)) 

Продукт теперь на самом деле однородна, так как каждая позиция содержит Bool, так hcollapse превращает его в обычный однородный список снова:

GHCi> hcollapse it 
[True,True,False] 

Последний шаг просто относится and к нему:

GHCi> and it 
False 
0

для этого очень конкретной ситуации (проверка набора атрибутов, которые являются простой суммой типовыми х годов с 0-Arity конструкторами), вы можете использовать следующую конструкцию с помощью Data.Data дженерики:

{-# LANGUAGE DeriveDataTypeable #-} 

module Signature where 

import Data.List (sort, transpose) 
import Data.Data 

data Color = Red | Green | Blue deriving (Eq, Show, Enum, Ord, Data) 
data Width = Thin | Normal | Fat deriving (Eq, Show, Enum, Ord, Data) 
data Height = Short | Medium | Tall deriving (Eq, Show, Enum, Ord, Data) 

data Object = Object { color :: Color 
        , width :: Width 
        , height :: Height } deriving (Show, Data) 

-- |Signature of attribute constructors used in object 
signature :: Object -> [String] 
signature = gmapQ (show . toConstr) 

uniqueAttributes :: [Object] -> Bool 
uniqueAttributes = all allDifferent . transpose . map signature 

allDifferent :: (Ord a) => [a] -> Bool 
allDifferent = comparePairwise . sort 
    where comparePairwise xs = and $ zipWith (/=) xs (drop 1 xs) 

Ключевым моментом здесь является функция signature, которая принимает объект и обобщенно через его непосредственные дочерние вычисляет имя конструктора каждого ребенка.Итак:

*Signature> signature (Object Red Fat Medium) 
["Red","Fat","Medium"] 
*Signature> 

Если есть какие-либо другие, чем эти типы просто сумма полей (как, скажем, атрибут типа data Weight = Weight Int или если вы добавили name :: String поле для Object), то это будет внезапно потерпеть неудачу.

(ред добавить :) Обратите внимание, что вы можете использовать constrIndex . toConstr вместо show . toConstr использовать индекс Int значного конструктора (в основном, индекс, начинающийся с 1 конструктор в data определении), если это чувствует себя менее косвенным , Если Constr, возвращенный toConstr, имел экземпляр Ord, не было бы никакой косвенности, но, к сожалению, ...

+0

Хотя преобразование конструкторов в строки и их сравнение как таковое несколько косвенно, это решение имеет силу довольно просто. Похоже, новые подходы к дженерикам - все это ярость, но «Data.Data» выполняет эту работу! – yun

+0

Я добавил примечание об использовании 'constrIndex', чтобы вместо этого получить индексы' Int'. Это то, что я сделал изначально, но использование 'show' давало более красивое (хотя и по общему признанию менее прямое и менее эффективное) значение подписи. –