我正在完成书中的练习"开始Haskell。"练习4-8是使二元搜索树成为Functor的一个实例并定义fmap。这就是树的样子:
data BinaryTree a = Node a (BinaryTree a) (BinaryTree a)
| Leaf
deriving Show
因为它是一个搜索树,所以树上的所有操作必须保持不变,即左子树中的所有值都是<节点的值和右子树中的所有值都是>节点的值。这意味着树中的所有值必须是序数(Ord a => BinaryTree a
)。
两个问题:
fmap :: (a -> b) -> BinaryTree a -> BinaryTree b
以来,我如何强制执行b
也是有序的?如果它不是一个Functor,我可以简单地fmapOrd :: (Ord a, Ord b) => (a -> b) -> BinaryTree a -> BinaryTree b
,但Functor类型类没有强制执行Ord约束。答案 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 a
和BinaryTree a
之间的同构现象,基本上是returnBin
和bindBin
。
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)
Functor
和fmap
的要点是,它适用于可以存储在数据结构中的所有a
和b
,就像{{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