能否将Haskell树类型泛化,以支持任意可遍历的子树存储实现?
基于类型族的多存储适配树结构实现方案
完全可以通过类型族抽离子树存储层的方式实现多类树结构,既保留跨实现的通用逻辑复用能力,也能针对特定存储做性能优化。
核心抽象结构
我们可以通过带关联类型的类型类,把树的公共结构和具体子树存储方式解耦:
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE DefaultSignatures #-} import Data.Kind (Type) import Data.Foldable (foldMap, traverse_) import Data.Monoid (Monoid(..)) class TreeLike t where -- 类型族关联:每个树实现绑定专属的子树存储容器类型,容器本身接受元素类型作为参数 type ChildContainer t :: Type -> Type -- 公共结构访问接口 getNodeMeta :: t a b -> (a, b) getChildren :: t a b -> ChildContainer t (t a b) -- 基于容器可遍历特性的默认实现,减少重复代码 foldChildren :: Monoid m => (t a b -> m) -> t a b -> m default foldChildren :: (Foldable (ChildContainer t), Monoid m) => (t a b -> m) -> t a b -> m foldChildren f node = foldMap f (getChildren node)
核心设计逻辑是不强制要求子树容器满足某个统一的存储协议,只需要在写通用函数时按需声明约束(比如要求容器可折叠、可遍历),完全不侵入每个树类型的内部存储实现。
适配自定义树类型
你已有的两个树结构可以直接实现上述类型类,不需要改动原有构造逻辑:
- 列表存储版本的树
data Tree a b = Node a b [Tree a b] deriving (Show, Foldable, Generic) instance TreeLike Tree where type ChildContainer Tree = [] getNodeMeta (Node meta val _) = (meta, val) getChildren (Node _ _ children) = children
- Massiv数组存储版本的树
import qualified Massiv as M data MassivTree a b = V a b (M.Array M.B M.Ix1 (MassivTree a b)) deriving (Show) instance TreeLike MassivTree where type ChildContainer MassivTree = M.Array M.B M.Ix1 getNodeMeta (V meta val _) = (meta, val) getChildren (V _ _ children) = children
编写通用无依赖函数
所有不关心子树存储方式的逻辑,只需要约束对应TreeLike实例的容器满足可遍历特性即可,列表存储和数组存储的树可以直接复用同一套代码:
- 树深度计算
treeDepth :: (TreeLike t, Foldable (ChildContainer t)) => t a b -> Int treeDepth node = 1 + maximum (0 : map treeDepth (foldChildren id node))
- 全树节点遍历
walkAllNodes :: (TreeLike t, Foldable (ChildContainer t)) => (t a b -> IO ()) -> t a b -> IO () walkAllNodes action node = do action node traverse_ (walkAllNodes action) (getChildren node)
针对特定存储的优化函数
依托类型族的类型绑定能力,你可以直接针对具体存储类型编写专用逻辑,不需要通过类的约束透传性能特性。比如针对Massiv数组的连续存储优势,实现O(1)时间的子节点索引访问,这个逻辑完全不需要兼容列表存储的树结构,类型系统会自动拦截不匹配的调用:
-- 仅适用于MassivTree的快速索引函数 getChildAt :: Int -> MassivTree a b -> Maybe (MassivTree a b) getChildAt idx (V _ _ children) = children M.!? idx
对比直接把容器作为树类型参数的写法(比如data GenTree c a b = Node a b (c (GenTree c a b))),类型族的关联类型可以把数组类容器自带的表示类型、索引类型等复杂参数全部收敛到实例定义里,不会让上层通用函数的签名充斥冗余的类型参数,类型推断的流畅度也高很多。后续如果需要新增其他存储类型的树(比如基于Vector存储子树的结构),只需要新增对应的TreeLike实例绑定容器类型,所有已写的通用函数可以直接复用,不需要修改原有逻辑。
内容的提问来源于stack exchange,提问作者loke202
相关产品推荐
相关产品推荐

