Haskell:有没有一种方法可以映射'在代数数据类型?

时间:2017-01-07 15:20:38

标签: haskell records algebraic-data-types

假设我有一些简单的代数数据(基本上是枚举)和另一种将这些枚举作为字段的类型。

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中很容易),但这些字段有不同的类型......

2 个答案:

答案 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返回的toConstrOrd个实例,则根本就没有间接,但不幸的是......