【问题标题】:Defining fmap for a binary search tree为二叉搜索树定义 fmap
【发布时间】:2014-04-06 20:36:59
【问题描述】:

我正在完成“Beginning Haskell”一书中的练习。练习 4-8 是让一个二叉搜索树成为 Functor 的一个实例,并定义 fmap。这就是树的样子:

data BinaryTree a = Node a (BinaryTree a) (BinaryTree a) 
                  | Leaf
                    deriving Show

因为它是一棵搜索树,所以对树的所有操作都必须保持左子树中的所有值都是节点的值的不变量。这意味着树中的所有值都必须是有序的 (Ord a => BinaryTree a)。

两个问题:

  1. 由于fmap :: (a -> b) -> BinaryTree a -> BinaryTree b,我如何强制b 也是序数?如果它不必是 Functor,我可以简单地使用 fmapOrd :: (Ord a, Ord b) => (a -> b) -> BinaryTree a -> BinaryTree b,但 Functor 类型类不强制执行 Ord 约束。
  2. 高效的实现是什么样的?我的第一个想法是折叠树,并根据映射值构建一棵新树。不幸的是,由于 (1),我没有走到这一步。

【问题讨论】:

  • 如果 TypeClasses 的实例可以对函数参数施加比原始声明更严格的约束,那就太好了。看来这应该是可能的......

标签: haskell tree functor


【解决方案1】:

如果你想强制排序,那么你的二叉树就不能变成函子,因为 - 正如你所指出的 - 类型不匹配。然而,虽然树不能是 keys 上的函子,但它可以是 上的函子,前提是每个都有单独的类型参数。标准Data.Map(也实现为搜索树)以这种方式工作。

-- Now the "v" parameter can be mapped over without any care for tree invariants
data Tree k v = Node k v (Tree k v) (Tree k v) | Leaf 

关于fmap的实现,你的第一个想法是对的。不过还有一种比较懒的方式,就是让 GHC 派生实例:

{-# LANGUAGE DeriveFunctor #-}

data Tree k v = Node k v (Tree k v) (Tree k v) | Leaf deriving (Functor)

它几乎总是符合您的意图,只需记住让最后一个类型参数成为您打算映射的那个。

【讨论】:

    【解决方案2】:

    Functors 和 fmap 的意义在于它适用于所有可以存储在您的数据结构中的 ab,就像 Monad 必须适用于所有类型 a以及。你的 Functor 实例应该是这样的

    instance Functor BinaryTree where
        fmap f Leaf = Leaf
        fmap f (Node a l r) = Node (f a) (fmap f l) (fmap f r)
    

    但是如果你想确保二叉树上的映射保持平衡,那么你需要一个函数

    balanceTree :: Ord a => BinaryTree a -> BinaryTree a
    

    你应该能够通过一些谷歌搜索相当容易地实现这个函数,然后你可以定义一个专门的映射函数

    binMap :: (Ord a, Ord b) => (a -> b) -> BinaryTree a -> BinaryTree b
    binMap f = balanceTree . fmap f
    

    然后你应该确保你和你图书馆的用户永远不要使用fmap(除非必要),而是使用binMap

    【讨论】:

    • binMap 不需要Ord a
    • 更糟糕的问题是fmap的结果不是搜索树,所以为搜索树定义的很多函数都不能正常工作,用户应该永远不要使用fmap 对于这种类型。确保这一点的最好方法是不要定义fmap :)
    • @J.Abrahamson 我只包含它是因为 OP 想要它,有时包含一个额外的条件,虽然不是绝对必要的,但对于确保 API 不被滥用很有用。
    【解决方案3】:

    我不会说我推荐以下内容,但为了完整性实际上可以定义这样的 Functor

    Functor 类型类要求您可以将fmap any 函数添加到您的Functor 中。这通常意味着很难确保需要类型类实例的不变量。但是,我们可以稍微扭曲这种情况,实际上最终得到一个Functor 实例。在实践中,这意味着我们可以使用类型系统来确保我们将重新平衡推迟到更方便的时间。

    首先,我们将介绍对上述类型的需求。特别是,我们将给它一个保持平衡的Monoid 实例。这很好,因为Monoid 不需要我们的容器是多态的。

    instance Ord a => Monoid (BalancedTree a) where
      mempty = Leaf
      mappend Leaf Leaf = Leaf
      mappend Leaf b    = b
      mappend b    Leaf = b
      mappend (Node a l1 r1) (Node b l2 r2) = ... -- merge and rebalance here
    

    现在,使用这个实例,我们可以编写函数,几乎对应于MonadMonad 实例BinaryTree。特别是,我们需要它来将我们的新树组合为使用bindBin 构建的,几乎 版本的(>>=) 在二叉搜索树上。

    returnBin :: a -> BinaryTree a
    returnBin a = Node a Leaf Leaf
    
    bindBin :: Ord b => BinaryTree a -> (a -> BinaryTree b) -> BinaryTree b
    bindBin Leaf _ = Leaf
    bindBin (Node a l r) f = bindBin l f <> f a <> bindBin r f
    

    然后我们介绍这个很奇怪的类型(需要RankNTypes扩展)

    newtype FBinaryTree a = 
      FBinaryTree (forall r . Ord r => (a -> BinaryTree r) -> BinaryTree r)
    

    对此有很多思考方式,但我们只注意到FBinaryTree aBinaryTree a 之间存在同构,基本上,returnBinbindBin 见证了这一点。

    toF :: BinaryTree a -> FBinaryTree a
    toF bt = FBinaryTree (bindBin bt)
    
    fromF :: Ord a => FBinaryTree a -> BinaryTree a
    fromF (FBinaryTree k) = k returnBin
    

    最后,由于FBinaryTree 继承了Cont monad 或Yoneda 引理类型的一些属性,我们可以为FBinaryTree 定义一个Functor 实例!

    instance Functor FBinaryTree where
      fmap f (FBinaryTree c) = FBinaryTree (\k -> c (k . f))
    

    所以现在,我们要做的就是将我们的BinaryTrees 转换为FBinaryTrees,在那里执行我们的Functor 操作,然后根据需要返回到BinaryTree。一帆风顺,对吧?

    嗯,差不多。事实证明,我们为此付出了巨大的效率代价。特别是,当使用像 FBinaryTree 这样的类型时,很容易发生指数级爆炸。我们可以通过使用不时发送FBinaryTreeBinaryTree 来避免这些情况

    optimize :: Ord a => FBinaryTree a -> FBinaryTree a
    optimize = toF . fromF
    

    正如类型显示的那样,它要求我们在那里有一个Ord 实例。事实上,代码将使用那里的Ord 实例来执行所需的重新平衡。

    【讨论】:

      【解决方案4】:

      要回答您的第一个问题,您不需要限制任一类型都是Ord 的成员。 add、search 和 remove 等功能仅适用于 Ord 的成员,但对于 fmap,无需进行比较。允许用户将树从一棵无与伦比的树转换为另一棵树并没有错。只是他无法对结果类型调用 add、remove 或 search。

      至于您的第二个问题,我的建议是使用递归。该函数将采用a 类型的树和一个函数,并返回一个带有该函数的新树,并返回一个将函数应用于值和 fmap 应用于其子级的树。这是一个简单的实现:

      fmap::(BinaryTree a,BinaryTree b)=>BinaryTree a->(a->b)->BinaryTree b
      fmap (Node value left right) fun=Node (fun value) (fmap left) (fmap right)
      fmap Leaf _ _=Leaf
      

      我不确定我的语法是否正确,但你明白了。

      【讨论】:

        【解决方案5】:

        另一种选择是使用 GADT 使 Ord a 约束成为类型的一部分:

        data BinaryTree a where
          Leaf :: BinaryTree a
          Node :: Ord a => a -> BinaryTree a -> BinaryTree a -> BinaryTree a
            deriving Show
        

        现在在Node 上进行模式匹配时,您可以使用约束。

        fmap _ Leaf = Leaf
        fmap f (Node value left right) = insert (f value) (merge (fmap f left) (fmap f right))
          -- assumes you defined insert and merge functions for search trees
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2017-09-28
          • 1970-01-01
          • 2023-03-08
          • 1970-01-01
          • 1970-01-01
          • 2010-10-26
          相关资源
          最近更新 更多