Suppose that I have some simple algebraic data (essentially enums) and another type which has these enums as fields.
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)
Given a list of objects, I want to test that the attributes are all distinct. For this I have following functions (using sort
from 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 ]
This works, but is rather dissatisfying because I had to type each field (color, width, height) manually. In my actual code, there are more fields! Is there a way of 'mapping' the function
\field -> allDifferent $ map field objects
over the fields of an algebraic datatype like Object
? I want to treat Object
as a list of its fields (something that would be easy in e.g. javascript), but these fields have different types...
For this very specific situation (checking a set of attributes that are simple sum types with 0-arity constructors), you can use the following construction using
Data.Data
generics:The key here is the function
signature
which takes an object and generically across its immediate children calculates the constructor name of each child. So:If there are any fields other than these simple sum types, (like say an attribute of type
data Weight = Weight Int
or if you added aname :: String
field toObject
), then this will suddenly fail.(Edited to add:) Note that you can use
constrIndex . toConstr
in place ofshow . toConstr
to use anInt
-valued constructor index (basically, the index starting with 1 of the constructor within thedata
definition), if this feels less indirect. If theConstr
returned bytoConstr
had anOrd
instance, there would be no indirection at all, but unfortunately...