假设我有一些简单的代数数据(基本上是枚举)和另一种将这些枚举作为字段的类型。
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 :(得分: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
的实例,这可以使用Template Haskell完成:
deriveGeneric ''Object
让我们试着通过一个具体的例子来看看这里发生了什么:
objects = [Object Red Thin Short, Object Green Fat Short]
第map (unZ . unSOP . from)
行将每个Object
转换为异构列表(在库中称为n-ary产品):
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
答案 1 :(得分: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
个实例,则根本就没有间接,但不幸的是......