【问题标题】:Haskell - Transform a list of unions into a tuple of listsHaskell - 将联合列表转换为列表元组
【发布时间】:2023-03-11 09:35:01
【问题描述】:

我正在寻找一种将列表转换为 n 元组的方法,其中一个列表用于不相交联合中的 n 个构造函数中的每一个。标准库专门为Eithers 定义了类似的函数:

partitionEithers :: [Either a b] -> ([a], [b])

我正在寻找解决具有以下要求的广义问题的技术:

  • 写起来方便
  • 尽可能少的样板代码
  • 一次性处理列表
  • 允许使用数据类型泛型、元编程、现有库等
示例

这是一个包含两个建议解决方案的示例规范:

partitionSum :: [MySum] -> ([A], [B], [C], [D])

data MySum
  = CaseA A
  | CaseB B
  | CaseC C
  | CaseD D

data A = A deriving Show
data B = B deriving Show
data C = C deriving Show
data D = D deriving Show

-- expect "([A,A],[B,B,B],[],[D])"
test :: IO ()
test = print . partitionSum $
  [CaseD D, CaseB B, CaseA A, CaseA A, CaseB B, CaseB B]

第一次尝试:n 个遍历列表 n 次的列表推导式。

partitionSum1 :: [MySum] -> ([A], [B], [C], [D])
partitionSum1 xs =
  ( [a | CaseA a <- xs]
  , [b | CaseB b <- xs]
  , [c | CaseC c <- xs]
  , [d | CaseD d <- xs]
  )

第二次尝试:单次遍历输入列表。我必须手动将状态穿过折叠,这使得解决方案有点重复且写起来很烦人。

partitionSum2 :: [MySum] -> ([A], [B], [C], [D])
partitionSum2 = foldr f ([], [], [], [])
  where
    f x (as, bs, cs, ds) =
      case x of
        CaseA a -> (a : as, bs, cs, ds)
        CaseB b -> (as, b : bs, cs, ds)
        CaseC c -> (as, bs, c : cs, ds)
        CaseD d -> (as, bs, cs, d : ds)

【问题讨论】:

标签: haskell functional-programming


【解决方案1】:

除了Representable answer:


看到foldr f ([], [], [], []) 后我想到的一个事情是定义一个幺半群,其中 nil 的情况是 mempty

{-# DerivingVia #-}
..
import GHC.Generics (Generically(..), ..)

type Classify :: Type
type Classify = C [A] [B] [C] [D]
  deriving
  stock Generic

  deriving (Semigroup, Monoid)
  via Generically Classify

-- mempty = C [] [] [] []
-- C as bs cs ds <> C as1 bs1 cd1 ds1 = C (as ++ as1) (bs ++ bs1) (cs ++ cs1) (ds ++ ds1)

Generically 将来会从GHC.Generics 导出。它通过通用的逐点提升将Classify 定义为半群和幺半群。

有了这个,您只需要一个分类器函数,它将MySum 分类为Classify,您可以根据foldMap 定义partition

classify :: MySum -> Classify
classify = \case
  SumA a -> C [a] [] [] []
  SumB b -> C [] [b] [] []
  SumC c -> C [] [] [c] []
  SumD d -> C [] [] [] [d]

partition :: Foldable f => f MySum -> Classify
partition = foldMap classify

【讨论】:

    【解决方案2】:

    由于您的函数是从总和到乘积的转换,因此使用generics-sop 有一个相当简单的实现。这是一个使用更专业的类型增强 GHC 泛型的库,使代数类型(即乘积之和)的归纳更简单。

    首先,前奏:

    {-# LANGUAGE DeriveGeneric, StandaloneDeriving #-}
    
    import Generics.SOP hiding ((:.:))
    import qualified GHC.Generics as GHC
    import GHC.Generics ((:.:)(..))
    
    
    partitionSum :: (Generic t) => [t] -> NP ([] :.: NP I) (Code t)
    

    这是您要编写的方法。让我们检查一下它的类型。

    • 单个参数是一些泛型类型的列表。很简单。请注意,Generic 是来自generics-sop,而不是来自 GHC
    • 返回值是一个 n 元乘积(n 元组),其中每个元素都是一个由 NP I 组成的列表(它本身就是一个 n 元乘积,因为通常代数数据类型构造函数可能有多个字段)
    • Code t 是 t 的积和类型表示。它是类型列表的列表。例如Code (Either a b) ~ '[ '[a], '[b] ]。 t 的通用值表示为 SOP I (Code t) - “代码”上的产品总和。

    要实现这一点,我们可以将每个t 转换为其通用表示,然后折叠结果列表:

    
    partitionSum = partitionSumGeneric . map from
    
    partitionSumGeneric :: SListI xss => [SOP I xss] -> NP ([] :.: NP I) xss
    partitionSumGeneric = foldr (\(SOP x) -> classifyGeneric x) emptyClassifier
    

    partitionSumGeneric 与partitionSum 几乎相同,但对值的通用表示进行操作。

    现在是有趣的部分。让我们从折叠的基本情况开始。这应该在每个位置都包含空列表。 generics-sop 提供了一种方便的机制,可以在每个位置生成具有统一值的产品类型:

    emptyClassifier :: SListI xs => NP ([] :.: NP I) xs
    emptyClassifier = hpure (Comp1 [])
    

    递归情况如下:如果值在索引k 处具有标记,则将该值添加到累加器中索引k 处的列表中。我们可以同时递归 sum 类型(它现在是通用的,所以类型为 NS (NP I) xs 的值 - 乘积的总和)和累加器。

    classifyGeneric :: NS (NP I) xss -> NP ([] :.: NP I) xss -> NP ([] :.: NP I) xss
    classifyGeneric (Z x)  (Comp1 l :* ls) = (Comp1 $ x : l) :* ls
    classifyGeneric (S xs) (      l :* ls) =              l  :* classifyGeneric xs ls
    

    您的示例添加了一些数据以使其更有趣:

    data MySum
      = CaseA A
      | CaseB B
      | CaseC C
      | CaseD D
    
    -- All that's needed for `partitionSum' to work with your type
    deriving instance GHC.Generic MySum
    instance Generic MySum
    
    data A = A Int deriving Show
    data B = B String Int deriving Show
    data C = C deriving Show
    data D = D Integer deriving Show
    
    test = partitionSum $
      [CaseD $ D 0, CaseB $ B "x" 1, CaseA $ A 2, CaseA $ A 3, CaseB $ B "y" 4, CaseB $ B "z" 5]
    

    结果是:

    Comp1 {unComp1 = [I (A 2) :* Nil,I (A 3) :* Nil]} :* Comp1 {unComp1 = [I (B "x" 1) :* Nil,I (B "y" 4) :* Nil,I (B "z" 5) :* Nil]} :* Comp1 {unComp1 = []} :* Comp1 {unComp1 = [I (D 0) :* Nil]} :*Nil
    

    【讨论】:

      猜你喜欢
      • 2011-03-05
      • 1970-01-01
      • 1970-01-01
      • 2020-02-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-06-05
      • 1970-01-01
      相关资源
      最近更新 更多