You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

是否存在可修改层级结构形状的Haskell类型类?

解决层级数据结构的重复递归处理问题

针对你遇到的递归数据结构中重复编写遍历逻辑的问题,有两种主要方案可以减少代码重复,提升扩展性:

方案一:自定义默认遍历函数/类型类

你可以先编写一个处理所有默认递归逻辑的辅助函数,只在需要特殊处理的构造器分支中自定义逻辑,其余情况复用默认逻辑:

1. 定义默认遍历函数

defaultWalk :: (Item a -> Item a) -> Item a -> Item a
defaultWalk f x@(Leaf _) = x
defaultWalk f (ListItems xs) = ListItems $ f <$> xs
defaultWalk f (DoubleItem x y) = DoubleItem (f x) (f y)
defaultWalk f (TripleItem x y z) = TripleItem (f x) (f y) (f z)

这个函数会递归处理所有子节点,并保持原构造器的结构,只有你在业务函数中覆盖的分支会被修改。

2. 简化业务函数

现在你的removeDoubles和removeTriples只需要编写特殊分支,其余情况直接调用defaultWalk:

removeDoubles :: Item a -> Item a
removeDoubles (DoubleItem x y) = ListItems [removeDoubles x, removeDoubles y]
removeDoubles x = defaultWalk removeDoubles x

removeTriples :: Item a -> Item a
removeTriples (TripleItem x y z) = ListItems [removeTriples x, removeTriples y, removeTriples z]
removeTriples x = defaultWalk removeTriples x

扩展性优化

如果后续给Item添加新的构造器,只需要更新defaultWalk函数,添加新构造器的默认处理逻辑即可,所有基于它的业务函数都会自动适配新结构,无需逐个修改。

如果需要在多个递归结构中复用这种模式,可以把defaultWalk抽象成类型类:

class RecursiveStructure t where
  defaultWalk :: (t a -> t a) -> t a -> t a

instance RecursiveStructure Item where
  defaultWalk f x@(Leaf _) = x
  defaultWalk f (ListItems xs) = ListItems $ f <$> xs
  defaultWalk f (DoubleItem x y) = DoubleItem (f x) (f y)
  defaultWalk f (TripleItem x y z) = TripleItem (f x) (f y) (f z)

方案二:使用Catamorphism(递归折叠)

这是函数式编程中处理递归结构的通用模式,通过分离递归遍历逻辑和结构转换逻辑,彻底消除重复代码。

1. 定义基函子

首先把Item的递归结构拆分为基函子(描述非递归的结构形状):

data ItemF a r =
    LeafF a
    | ListItemsF [r]
    | DoubleItemF r r
    | TripleItemF r r r
    deriving (Functor)

这里r代表递归位置的占位符,原来的Item a可以用Fix类型包装基函子得到:

newtype Fix f = Fix { unFix :: f (Fix f) }
type Item a = Fix (ItemF a)

2. 通用递归折叠函数

利用Functor实例,定义通用的catamorphism函数,它会自动处理递归遍历:

cata :: Functor f => (f b -> b) -> Fix f -> b
cata alg = alg . fmap (cata alg) . unFix

3. 编写结构转换代数

每个业务逻辑对应一个"代数"函数,只需要定义每个构造器的转换规则,无需手动处理递归:

-- removeDoubles对应的代数
removeDoublesAlg :: ItemF a (Item a) -> Item a
removeDoublesAlg (LeafF a) = Fix $ LeafF a
removeDoublesAlg (ListItemsF xs) = Fix $ ListItemsF xs
removeDoublesAlg (DoubleItemF x y) = Fix $ ListItemsF [x, y]
removeDoublesAlg (TripleItemF x y z) = Fix $ TripleItemF x y z

removeDoubles :: Item a -> Item a
removeDoubles = cata removeDoublesAlg

-- removeTriples对应的代数
removeTriplesAlg :: ItemF a (Item a) -> Item a
removeTriplesAlg (LeafF a) = Fix $ LeafF a
removeTriplesAlg (ListItemsF xs) = Fix $ ListItemsF xs
removeTriplesAlg (DoubleItemF x y) = Fix $ DoubleItemF x y
removeTriplesAlg (TripleItemF x y z) = Fix $ ListItemsF [x, y, z]

removeTriples :: Item a -> Item a
removeTriples = cata removeTriplesAlg

这种方式的优势在于:所有递归遍历逻辑都由cata统一处理,业务函数只需要关注结构转换规则;后续添加新构造器时,只需要更新ItemF和对应的代数函数,扩展性极强。


内容的提问来源于stack exchange,提问作者Chris J Harris

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 22:52:50