【问题标题】:What's a better way of managing large Haskell records?管理大型 Haskell 记录的更好方法是什么?
【发布时间】:2016-07-07 14:53:16
【问题描述】:

用字母替换字段名称,我有这样的情况:

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 都是相同的类型,所以我可以将它们放在一个列表中,但它们可以是许多不同的类型。我对无需大量模式即可折叠记录的任何方式特别感兴趣。

我正在使用这个大记录来保存permutation parsing the vCard format 产生的不同类型。

更新

我已经实现了下面建议的genericsfoldl 方法。它们都有效,并且都将three large field lists 减为1。

【问题讨论】:

  • 记录字段是否需要命名(Foo 可以是记录以外的其他名称)吗?
  • 这种 XY 问题的味道。也许解决方案是根本没有那种Foo,但要做出这种判断,我们必须获得有关Foo必须解决的问题的信息。
  • 似乎是数据类型泛型编程的一个用例,可能使用像“generics-sop”这样的泛型库。
  • @Bakuriu - 好点。我添加了一条关于我要解决的问题的注释。基本上,记录已经到位,可以收集permutation parsing the vCard format 产生的不同类型。在我链接到你的代码中,你可以看到它是如何变得笨拙的。我不知道我能确定类型只会是列表和 Maybes - 这是一个早期而粗略的 impl - 但也许可以以某种方式利用只有两种类型的事实......

标签: haskell records


【解决方案1】:

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

也许记录中的所有字段都实现了一些我们想要使用的类型类(典型的例子是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 函数将二元运算提升为泛型-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)

Nil:* 构造函数用于构建运算符向量(该类型称为 NP,来自 n 元积)。如果向量与记录中的字段数不匹配,则程序将无法编译。

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

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 分支中的 implemented this approach 与 foldl 分支进行比较。
  • @rcampbell 我已经更新了代码以简化一些事情。
【解决方案2】:

一些建议:

(1) 您可以使用 RecordWildCards 扩展来自动 将记录解包到变量中。如果您需要打开包装,则无济于事 两个相同类型的记录,但记住这一点很有用。 Oliver Charles 有一篇不错的博文:(link)

(2) 您的示例应用程序似乎正在对记录执行折叠。 看看 Gabriel Gonzalez 的 foldl 包。还有一篇博文:(link)

这是一个示例,说明如何将它与以下记录一起使用:

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

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

lmap _a (L.Fold max 0 id)

是相同的折叠,但在我们使用 _aFoo 记录列表上 生成整数。

【讨论】:

猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-10-08
  • 2017-04-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多