为二叉搜索树定义fmap

时间:2014-04-06 20:37:00

标签: haskell tree functor

我正在完成书中的练习"开始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),我没有做到这一点。

5 个答案:

答案 0 :(得分:4)

如果你想强制执行排序,那么你的二进制树就不能成为一个仿函数,因为 - 正如你所指出的 - 这些类型不匹配。但是,虽然树不能是上的仿函数,但它可以是的仿函数,前提是每个都有单独的类型参数。标准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)

它几乎总是匹配你的意图,只记得让最后一个类型参数成为你想要映射的那个。

答案 1 :(得分:4)

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

Functor类型类要求您可以fmap 任何函数进入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

现在,使用此实例,我们可以编写将几乎对应Monad 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引理类型的某些属性时,我们可以为Functor定义FBinaryTree个实例}!

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

现在,我们所要做的就是将BinaryTree转换为FBinaryTree s,在那里执行Functor次操作,然后再回到BinaryTree需要。顺风顺水,对吧?

好吧,差不多。事实证明,我们为此付出了巨大的效率代价。特别是,当使用类似FBinaryTree的类型时,很容易发生指数性爆炸。我们可以通过使用

不时通过FBinaryTree通过BinaryTree来避免这些问题
optimize :: Ord a => FBinaryTree a -> FBinaryTree a
optimize = toF . fromF

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

答案 2 :(得分:3)

Functorfmap的要点是,它适用于可以存储在数据结构中的所有ab,就像{{1}一样}必须也适用于所有类型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

答案 3 :(得分:0)

要回答您的第一个问题,您无需制定任何类型都是Ord成员的约束。添加,搜索和删除等功能仅适用于Ord的成员,但对于fmap,则无需进行比较。允许用户将树从一个无比的转换为另一个是没有错的。只是他无法调用添加,删除或搜索结果类型。

关于你的第二个问题,我的建议是使用递归。该函数将采用类型为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

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

答案 4 :(得分:0)

还有一个选择是使用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