如何在Haskell中一般遍历嵌套记录查找特定值?

时间:2016-12-03 21:04:40

标签: haskell generics

假设我有以下数据类型:

data Person = Person
  { personName :: String
  , personAddress :: Maybe PersonAddress
  }

data PersonAddress = PersonAddress
  { personAddressStreet :: String
  , personAddressStreet1 :: Maybe String
  , personAddressStreet2 :: Maybe PersonAddressStreet2
  }

data PersonAddressStreet2 = PersonAddressStreet2
  { personAddress2StreetStreet :: Maybe String
  , personAddress2StreetNumber :: Maybe Int
  }

有没有办法以通用方式遍历Person类型的值并报告哪些特定字段的值为Nothing

理想情况下,我希望看到嵌套结构中找到值的完整路径(例如(Person) personAddress -> (PersonAddress) personAddressStreet1

我查看了Typeable / Generic机器,虽然它似乎与我试图做的事情有关,但我并不清楚如何在这里使用它们。

很高兴收到任何建议或指示。

1 个答案:

答案 0 :(得分:5)

Generic是实现这一目标的方法。但是,你的问题仍有一些含糊之处。我将列出这些,以及我假设您希望它们被解决的方式

  • 在这种情况下,路径的类型是什么?我现在只打算[String]。每个String表示构造函数名称或字段名称。
  • 如果没有字段名称会怎样?我要将其记录为字段名称"no-field-name"
  • 我们应该走多远?例如,如果您有来自其他外部模块的数据构造函数,那么我们是否也应该搜索Nothing字段?我打算创建一个类型系列,它将类型映射到我们是否应该进入类型。

由于这个解决方案有点长,我用段落分隔了它。 我们将从一堆导入和编译指示以及包含函数nothingFields的类开始。

{-# LANGUAGE DeriveGeneric, TypeFamilies, FlexibleContexts, 
             MultiParamTypeClasses, TypeInType, FlexibleInstances,          
             TypeOperators, ScopedTypeVariables, UndecidableInstances 
  #-}
import GHC.Generics
import GHC.TypeLits
import Data.Proxy

-- List of constructor or field names to descend to the right field
type Field = [String]

class NothingFields a where
  nothingFields :: a -> [Field]

接下来,我们将创建一个类型系列,将类型映射到布尔值,说明我们是否要深入挖掘类型以查找Nothing字段。捕获所有默认情况(最后一个)是停止挖掘。

 type family StopDigging a :: Bool where
   StopDigging Person = False
   StopDigging PersonAddress = False
   StopDigging PersonAddressStreet2 = False
   StopDigging [a] = StopDigging a
   StopDigging (Maybe a) = StopDigging a
   StopDigging a = True

现在,我们想要一个NothingFields实例和一个帮助类NothingFields'来分支我们是否有一个我们应该尝试探索的字段。请注意,这是well-documented problem and there are tricks to solve it

-- This instance always matches because of its general instance head.
-- It dispatches to the right version of `nothingFields'` based on
-- whether  the `StopDigging` type family returns true or false.
instance (flag ~ StopDigging a, NothingFields' a flag) => NothingFields a where
  nothingFields = nothingFields' (Proxy :: Proxy flag)

-- Helper class whose instances' heads have different flags.
class NothingFields' a (flag :: Bool) where
  nothingFields' :: proxy flag -> a -> [Field]

-- Stop digging into fields
instance NothingFields' a True where
  nothingFields' _ _ = []

-- Continue digging into fields
instance (Generic a, GNothingFields' (Rep a)) => NothingFields' a False where
   nothingFields' _ = gNothingFields . from

最后一个例子是通用编程开始的地方。按照习惯,我们会为此制作一个GNothingFields'课程。在大多数情况下,填写实例非常简单。

-- Generic helper class corresponding to `NothingFields'`
class GNothingFields' f where
  gNothingFields :: f a -> [Field]

-- constructors without arguments
instance GNothingFields' U1 where
  gNothingFields U1 = []

-- sum of constructors
instance (GNothingFields' f, GNothingFields' g) => GNothingFields' (f :+: g) where
  gNothingFields (L1 x) = gNothingFields x
  gNothingFields (R1 x) = gNothingFields x

-- product; multiple fields
instance (GNothingFields' f, GNothingFields' g) => GNothingFields' (f :*: g) where
  gNothingFields (x :*: y) = gNothingFields x ++ gNothingFields y

其余案例包括:M1表示元数据,K1表示字段中的实际数据。这是真正的技巧将要发生的地方。 M1元数据位于数据类型,构造函数和记录周围。我们只想跟踪最后两个:

-- The `D` tells us this is datatype metadata.
instance GNothingFields' f => GNothingFields' (M1 D t f) where
  gNothingFields (M1 x) = gNothingFields x

-- The `C` tells us this is constructor metadata, so we extract
-- the constructor name using `symbolVal`.
instance (KnownSymbol constructor, GNothingFields' f) => GNothingFields' (M1 C ('MetaCons constructor a b) f) where
  gNothingFields (M1 x) = (symbolVal (Proxy :: Proxy constructor) :) <$> gNothingFields x

-- The `S` tells us this is record field metadata, but the `Nothing`
-- tells us the field has no name.
instance (GNothingFields' f) => GNothingFields' (M1 S ('MetaSel ('Nothing) a b c) f) where
  gNothingFields (M1 x) = ("no field name" :) <$> gNothingFields x

-- The `S` tells us this is record field metadata, and the `Just`
-- tells us the field has a name, so we extract that using `symbolVal`.
instance (KnownSymbol selector, GNothingFields' f) => GNothingFields' (M1 S ('MetaSel ('Just selector) a b c) f) where
  gNothingFields (M1 x) = (symbolVal (Proxy :: Proxy selector) :) <$> gNothingFields x

-- This represents an actual data field of type `Maybe`. Note we
-- recurse using our initial `nothingFields` and not `gNothingFields`.
instance {-# OVERLAPPING  #-} (NothingFields a) => GNothingFields' (K1 i (Maybe a)) where
  gNothingFields (K1 Nothing) = [[]]
  gNothingFields (K1 (Just x)) = nothingFields x

-- This represents an actual data field of type _not_ `Maybe`. Note we
-- recurse using our initial `nothingFields` and not `gNothingFields`.
instance (NothingFields a) => GNothingFields' (K1 i a) where
  gNothingFields (K1 x) = nothingFields x

现在,试试这个:

ghci> nothingFields (Person "name" Nothing)
[["Person","personAddress"]] 
ghci> nothingFields (Person "name" (Just (PersonAddress "addr" Nothing Nothing)))
[["Person","personAddress","PersonAddress","personAddressStreet1"],
 ["Person","personAddress","PersonAddress","personAddressStreet2"]]
ghci> nothingFields (Person "name" (Just (PersonAddress "addr" (Just "street1") Nothing)))
[["Person","personAddress","PersonAddress","personAddressStreet2"]]
ghci> nothingFields (Person "name" (Just (PersonAddress "addr" Nothing (Just (PersonAddressStreet2 Nothing Nothing)))))
[["Person","personAddress","PersonAddress","personAddressStreet1"],
 ["Person","personAddress","PersonAddress","personAddressStreet2","PersonAddressStreet2","personAddress2StreetStreet"],     
 ["Person","personAddress","PersonAddress","personAddressStreet2","PersonAddressStreet2","personAddress2StreetNumber"]]

声明

实施起来很有趣,但你确定这真的你想要什么吗?过去是一个调试工具,我不确定这真的有多大功能......无论如何 - 享受!