【问题标题】:Haskell: is there a way of 'mapping' over an algebraic data type?Haskell:有没有办法在代数数据类型上“映射”?
【发布时间】:2017-05-22 05:40:26
【问题描述】:

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

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)

给定一个对象列表,我想测试这些属性是否都是不同的。为此,我有以下功能(使用来自Data.List 的sort)

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

【问题讨论】:

  • 可以使用废品你的样板。对于这个简单的案例,我不确定这是否更好。
  • 你可以在没有泛型的情况下考虑它:uniqueAttributes objects = and [go color, go width, go height] where go :: (Ord a) => (Object -> a) -> Bool; go f = allDifferent (map f objects)

标签: haskell records algebraic-data-types


【解决方案1】:

这是使用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 元积):

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

【讨论】:

    【解决方案2】:

    对于这种非常特殊的情况(使用 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 值的构造函数索引(基本上,索引以data 定义中构造函数的1 开头),如果这感觉不那么间接的话。如果toConstr 返回的Constr 有一个Ord 实例,则根本不会有间接性,但不幸的是......

    【讨论】:

    • 虽然将构造函数转换为字符串并对其进行比较感觉有些间接,但此解决方案的优点是非常简单。似乎新的泛型方法风靡一时,但 Data.Data 可以胜任!
    • 我添加了一条关于使用constrIndex 来获取Int 值索引的注释。这是我最初所做的,但使用show 给出了一个更漂亮的(虽然公认的不那么直接和效率较低)的签名值。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-03-17
    • 2021-01-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-05-16
    相关资源
    最近更新 更多