如何更好地管理大型Haskell记录?

4

将字段名替换成字母后,我遇到了如下情况:

data Foo = Foo { a :: Maybe ...
               , b :: [...]
               , c :: Maybe ...
               , ... for a lot more fields ...
               } deriving (Show, Eq, Ord)

instance Writer Foo where
  write x = maybeWrite a ++
            listWrite  b ++
            maybeWrite c ++
            ... for a lot more fields ...

parser = permute (Foo
                   <$?> (Nothing, Just `liftM` aParser)
                   <|?> ([], bParser)
                   <|?> (Nothing, Just `liftM` cParser)
                   ... for a lot more fields ...

-- this is particularly hideous
foldl1 merge [foo1, foo2, ...]
merge (Foo a b c ...seriously a lot more...)
      (Foo a' b' c' ...) = 
        Foo (max a a') (b ++ b') (max c c') ...

有哪些技术可以帮助我更好地管理这种增长?

在完美的世界中,abc都是同一类型,因此我可以将它们保存在一个列表中,但它们可能是许多不同的类型。我特别关注任何一种可以折叠记录而不需要大量模式的方法。

我正在使用这个大型记录来保存从排列解析vCard格式中得到的不同类型。

更新

我已经实现了下面建议的通用foldl方法。它们都有效,并且都将三个大字段列表减少为一个。


记录字段需要命名吗(Foo可以是除记录之外的其他内容吗)? - Alec
1
这似乎是 XY 问题。也许解决方案并不是完全消除 Foo,而是我们必须获得关于 Foo 需要解决的问题的信息才能做出判断。 - Bakuriu
1
看起来这是一种数据类型通用编程的用例,可能需要使用像“generics-sop”这样的泛型库。 - danidiaz
@Bakuriu - 很好的观点。我已经添加了一个关于我试图解决的问题的注释。基本上,该记录是为了收集由排列解析vCard格式而产生的不同类型。在我链接的代码中,您可以看到它如何变得笨重。我不知道我能有多确定这些类型只会是列表和Maybes-这是一个早期和粗糙的实现-但也许只有两种类型可以以某种方式利用... - Robert Campbell
2个回答

5

数据类型通用编程技术可以以某种“统一”的方式转换记录的所有字段。

也许记录中的所有字段都实现了我们想要使用的某个类型类(典型的例子是Show)。或者我们有另一个“类似”的形状的记录,其中包含函数,并且我们想将每个函数应用于原始记录的相应字段。

对于这些用途,generics-sop库是一个很好的选择。它通过额外的类型级机制扩展了GHC的默认泛型功能,提供了类似于sequenceap等函数的类比,但可以作用于记录的所有字段。

使用generics-sop,我尝试创建一个稍微不那么冗长的merge函数版本。一些初步的导入:
{-# language TypeOperators #-}
{-# language DeriveGeneric #-}
{-# language TypeFamilies #-}
{-# language DataKinds #-}

import Control.Applicative (liftA2)
import qualified GHC.Generics as GHC
import Generics.SOP

一个helper函数,将二元操作提升到可以被generics-sop函数使用的形式:

fn_2' :: (a -> a -> a) -> (I -.-> (I -.-> I)) a -- I is simply an Identity functor
fn_2' = fn_2 . liftA2

一个通用的合并函数,接受操作符向量并适用于任何单构造器记录类型,该类型派生自Generic

merge :: (Generic a, Code a ~ '[ xs ]) => NP (I -.-> (I -.-> I)) xs -> a -> a -> a 
merge funcs reg1 reg2 =
    case (from reg1, from reg2) of 
        (SOP (Z np1), SOP (Z np2)) -> 
            let npResult  = funcs `hap` np1 `hap` np2
            in  to (SOP (Z npResult))

Code是一种类型族,返回描述数据类型结构的列表列表的类型级别列表。外部列表用于构造函数,内部列表包含每个构造函数的字段类型。

约束条件中的Code a ~ '[xs]部分表示“数据类型只能有一个构造函数”,通过要求外部列表恰好有一个元素来实现这一点。

(SOP (Z _)模式匹配从记录的通用表示中提取(异构)字段值向量。SOP代表“和乘积”。

具体例子:

data Person = Person
    {
        name :: String
    ,   age :: Int
    } deriving (Show,GHC.Generic)

instance Generic Person -- this Generic is from generics-sop

mergePerson :: Person -> Person -> Person
mergePerson = merge (fn_2' (++) :* fn_2' (+) :* Nil)
< p > Nil:*构造函数用于构建运算符向量(类型称为NP,来自n元乘积)。如果向量与记录中的字段数不匹配,则程序将无法编译。

更新。 鉴于您记录中的类型非常统一,创建操作向量的另一种方法是为每个字段类型定义辅助类型类的实例,然后使用hcpure函数:

class Mergeable a where
    mergeFunc :: a -> a -> a

instance Mergeable String where
    mergeFunc = (++)

instance Mergeable Int where
    mergeFunc = (+)

mergePerson :: Person -> Person -> Person
mergePerson = merge (hcpure (Proxy :: Proxy Mergeable) (fn_2' mergeFunc))

hcliftA2 函数(结合了 hcpurefn_2hap)可以用来进一步简化事情。


我已经在“generics”分支中实现了这种方法,以便与foldl进行比较。 - Robert Campbell
@rcampbell 我已经更新了代码,简化了一些东西。 - danidiaz

2

一些建议:

(1) 您可以使用RecordWildCards扩展自动将记录解压缩为变量。如果您需要解压缩两个相同类型的记录,则无法帮助,但它是一个有用的技巧。Oliver Charles在他的博客上写了一篇不错的文章:(链接)

(2) 看起来您的示例应用程序正在对记录执行fold操作。请查看Gabriel Gonzalez的foldl包。还有一篇博客文章:(链接)

以下是您可能如何将其与记录一起使用的示例:

data Foo = Foo { _a :: Int, _b :: String }

以下代码计算_a字段的最大值和_b_字段的连接。
 import qualified Control.Foldl as L
 import Data.Profunctor

 data Foo = Foo { _a :: Int, _b :: String }
  deriving (Show)

 fold_a :: L.Fold Foo Int
 fold_a = lmap _a (L.Fold max 0 id)

 fold_b :: L.Fold Foo String
 fold_b = lmap _b (L.Fold (++) "" id)

 fold_foos :: L.Fold Foo Foo
 fold_foos = Foo <$> fold_a <*> fold_b

 theFoos = [ Foo 1 "a", Foo 3 "b", Foo 2 "c" ]

 test = L.fold fold_foos theFoos

请注意使用Profunctor函数lmap来提取我们想要折叠的字段。表达式:
L.Fold max 0 id

fold是对Ints(或任何Num实例)列表的折叠,因此:

lmap _a (L.Fold max 0 id)

这里的情况与之前相同,只不过是针对一个Foo记录列表进行操作,我们使用_a来生成整数。


1
我已经在foldl分支中实现了这种方法,以便与通用方法进行比较。 - Robert Campbell

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接