如何基于generics-sop实现产品类型访问器的泛型遍历函数?
问题
我已经用generics-sop库实现了如下函数,它接收产品类型的实例值,遍历其所有成员并应用指定函数,最终返回结果列表:
import Generics.SOP qualified as SOP import Generics.SOP hiding (Generic) productToList :: forall c a t xs. ( IsProductType a xs, SOP.Generic a, ProductCode a ~ xs, All c xs ) => (forall b. c b => b -> t) -> a -> [t] productToList f = hcollapse . hcmap (Proxy :: Proxy c) (mapIK f) . productTypeFrom
示例用法如下:
import Generics.SOP qualified as SOP import Generics.SOP hiding (Generic) data MyNums = MyNums { anInt :: Int, anInteger :: Integer, aFloat :: Float } deriving stock Generic deriving anyclass SOP.Generic class (Eq a, Num a) => EqNum a instance (Eq a, Num a) => EqNum a c = MyNums { anInt = 0, anInteger = 5, aFloat = 7.2 } f :: (Eq a, Num a) => a -> Bool f x = x == 0 y :: [Bool] y = productToList @EqNum f c
该示例会输出列表[True, False, False]。
现在需要实现一个签名不同的函数:
productAccessorsToList :: ... => (forall b. c b => (a -> b) -> t) -> [t]
该函数无需传入产品类型的实例值,而是遍历目标类型的所有访问器函数,将每个访问器传入指定处理函数后返回结果列表。请问该函数的实现方式是什么?
解答
完整实现
我们可以借助generics-sop的元数据查询和泛型遍历能力来实现这个函数,核心是生成对应类型的所有字段访问器,再传入处理函数转换:
import Generics.SOP qualified as SOP import Generics.SOP hiding (Generic) import Generics.SOP.Metadata (constructorInfo, FieldInfo(..)) import Data.Proxy (Proxy(..)) -- 辅助类型:用于标记字段的类型 data FieldProxy a = FieldProxy productAccessorsToList :: forall c a t xs. ( SOP.Generic a, Code a ~ '[xs], -- 限制为单构造函数的记录/产品类型 All c xs, SOP.HasDatatypeInfo a ) => (forall b. c b => (a -> b) -> t) -> [t] productAccessorsToList f = hcollapse $ hcmap (Proxy @c) (mapIK f) accessors where -- 生成所有字段的访问器函数 accessors :: NP (K (a -> b)) xs accessors = hliftA mkAccessor fieldProxies where mkAccessor :: FieldProxy b -> K (a -> b) xs mkAccessor _ = K $ \x -> SOP.hindex (SOP.unSOP $ SOP.from x) (Proxy @b) -- 从类型元数据中获取每个字段的类型代理 fieldProxies :: NP FieldProxy xs fieldProxies = case constructorInfo (Proxy @a) of SOP.Constructor _ (SOP.Record fields) -> hmap (\_ -> FieldProxy) fields _ -> error "productAccessorsToList仅支持记录风格的产品类型"
关键细节说明
约束解析:
SOP.Generic a:让类型a具备泛型表示能力Code a ~ '[xs]:确保目标类型是单构造函数的产品类型(符合记录类型的结构)All c xs:要求所有字段类型都满足约束cSOP.HasDatatypeInfo a:用于读取类型的元数据(比如字段名)
访问器生成:
- 通过
constructorInfo获取记录的字段元数据,生成对应每个字段的FieldProxy(标记字段类型) - 用
hliftA将每个FieldProxy转换为访问器函数:\x -> hindex (unSOP (from x)) proxy,这个函数能从实例x中提取对应字段的值 - 最后通过
hcmap将处理函数f应用到每个访问器,再用hcollapse把泛型列表NP转换为普通Haskell列表
- 通过
示例用法
基于之前的MyNums类型,我们可以这样使用这个函数:
-- 处理函数:接收访问器,返回字段名和访问器的判断结果 processAccessor :: EqNum b => (MyNums -> b) -> (String, Bool) processAccessor getter = (getFieldName getter, getter c == 0) where -- 从访问器推导字段名 getFieldName :: (MyNums -> b) -> String getFieldName _ = case constructorInfo (Proxy @MyNums) of SOP.Constructor _ (SOP.Record fields) -> case hindex fields (Proxy @b) of FieldInfo name -> name _ -> "" -- 获取结果列表 result :: [(String, Bool)] result = productAccessorsToList @EqNum processAccessor
运行后result的值为[("anInt", True), ("anInteger", False), ("aFloat", False)]。
内容的提问来源于stack exchange,提问作者Clinton
相关产品推荐
相关产品推荐

