我最近阅读了[1]和[2],它们谈到了组织形态(和动力学),它们是递归方案,可以表达例如动态编程。不幸的是,如果您不了解类别理论,那么这些论文是不可访问的,即使其中的代码看起来像Haskell。
有人可以用一个使用真实Haskell代码的例子来解释histomorphisms吗?
答案 0 :(得分:19)
让我们首先定义一个我们将用作示例的数据类型:
data Nat = S Nat | Z
此数据类型编码Peano风格的自然数字。这意味着我们有0和一种生成任何自然数的后继的方法。
我们可以轻松地从整数构造新的自然数:
-- Allow us to construct Nats
mkNat :: Integer -> Nat
mkNat n | n < 0 = error "cannot construct negative natural number"
mkNat 0 = Z
mkNat n = S $ mkNat (n-1)
现在,我们首先要定义这种类型的变形,因为组织形态与它非常相似,并且变形更容易理解。
一种变形可以“折叠”或“拆除”一个结构。它只需要一个知道当所有递归项已经折叠时如何折叠结构的函数。让我们定义这样一个类型,类似于Nat,但所有递归实例都被类型a
的某个值替换:
data NatF a = SF a | ZF -- Aside: this is just Maybe
现在,我们可以为Nat定义我们的变形类型:
cata :: (NatF a -> a)
-> (Nat -> a)
给定一个知道如何将非递归结构NatF a
折叠到a
的函数,cata
将其转换为折叠整个Nat
的函数。< / p>
cata的实现非常简单:首先折叠递归子项(如果有的话)并应用我们的函数:
cata f Z = f ZF -- No subterm to fold, base case
cata f (S subterm) = f $ SF $ cata f subterm -- Fold subterm first, recursive case
我们可以使用此catamorphism将Nat
转换回Integer
s,如下所示:
natToInteger :: Nat -> Integer
natToInteger = cata phi where
-- We only need to provide a function to fold
-- a non-recursive Nat-like structure
phi :: NatF Integer -> Integer
phi ZF = 0
phi (SF x) = x + 1
因此,使用cata
,我们可以访问immediate子项的值。但是想象一下,我们也喜欢访问传递子项的值,例如,在定义斐波纳契函数时。然后,我们不仅需要访问先前的值,还需要访问前一个值的第二个值。这就是组织形态发挥作用的地方。
组织形态(histo听起来很像“历史”)允许我们访问所有以前的值,而不仅仅是最近的值。这意味着我们现在得到一个值列表,而不仅仅是一个值,所以histomorphism的类型是:
-- We could use the type NatF (NonEmptyList a) here.
-- But because NatF is Maybe, NatF (NonEmptyList a) is equal to [a].
-- Using just [a] is a lot simpler
histo :: ([a] -> a)
-> Nat -> a
histo f = head . go where
-- go :: Nat -> [a] -- This signature would need ScopedTVs
go Z = [f []]
go (S x) = let subvalues = go x in f subvalues : subvalues
现在,我们可以按如下方式定义fibN
:
-- Example: calculate the n-th fibonacci number
fibN :: Nat -> Integer
fibN = histo $ \x -> case x of
(x:y:_) -> x + y
_ -> 1
除此之外:即使它看起来如此,但是histo并不比cata更强大。你可以通过在cata和其他方面实现histo来看到自己。
我在上面的示例中未显示的是,如果您将类型定义为仿函数的固定点,则cata
和histo
可以非常普遍地实现。我们的Nat
类型只是Functor NatF
的固定点。
如果您以通用方式定义histo
,那么您还需要在我们的示例中提出类似NonEmptyList
的类型,但对于任何仿函数。这种类型正好是Cofree f
,其中f
是您采用固定点的函子。您可以看到它适用于我们的示例:NonEmptyList
只是Cofree Maybe
。这就是你如何使用通用类型histo
:
histo :: Functor f
=> (f (Cofree f a) -> a)
-> Fix f -- ^ This is the fixed point of f
-> a
您可以将f (Cofree f a)
视为一种堆栈,在每个“层”中,您可以看到折叠较少的结构。在堆栈的顶部,每个直接的子项都被折叠。然后,如果你深入一层,直接的子项不再折叠,但子子项已经全部折叠(或评估,这在AST的情况下更有意义)。所以你基本上可以看到已经应用的“减少序列”(=历史)。
答案 1 :(得分:12)
我们可以将其视为从cata
到histo
到dyna
的概括连续体。在recursion-schemes
:
Foldable t => (Base t a -> a) -> (t -> a) -- (1)
Foldable t => (Base t (Cofree (Base t) a) -> a) -> (t -> a) -- (2)
Functor f => (f (Cofree f a) -> a) -> (t -> f t) -> (t -> a) -- (3)
其中(1)是cata
,(2)是histo
,(3)是dyna
。这种概括的高级概述是histo
通过维护所有部分“正确折叠”的历史来改进cata
,并通过让任何类型操作来dyna
改进histo
t
只要我们可以为f
- 代数制作Foldable
,而不仅仅是Base t
个{具有通用Foldable
- 余代数作为cata
证明数据类型是最终的余代数。)
我们几乎可以通过查看实现其类型所需的内容来阅读其属性。
例如,foldr
的经典用法是定义data instance Prim [a] x = Nil | Cons a x
type instance Base [a] = Prim [a]
instance Foldable [a] where
project [] = Nil
project (a:as) = Cons a as
foldr :: (a -> b -> b) -> b -> [a] -> b
foldr cons nil = cata $ \case
Nil -> nil
Cons a b -> cons a b
foldr
重要的是,我们注意到cata
通过仅使用“先前”右侧折叠值来生成“下一个”部分右侧折叠值。这就是为什么它可以使用histo
来实现的:它只需要最前面的部分折叠结果。
当cata
概括histo
时,我们应该能够对它做同样的事情。这是基于foldr
的{{1}}
foldr :: (a -> b -> b) -> b -> [a] -> b
foldr cons nil = histo $ \case
Nil -> nil
Cons a (b :< _) -> cons a b
我们可以看到,我们不再立即拥有前一个折叠结果,而是必须到达Cofree
的第一层才能找到它。但是Cofree
是一个流,并且包含可能无限多的“先前折叠值”,我们可以根据自己的喜好深入挖掘它。这就是histo
赋予其“历史”力量的原因。例如,我们可以使用tail
编写一个相当直接的histo
,单独使用cata
更难以做到:
tail :: [a] -> Maybe [a]
tail = histo $ \case
Nil -> Nothing -- empty list
Cons _ (b :< x) -> case x of
Nil -> Just [] -- length 1 list
Cons a _ -> fmap (a:) b
这种风格有点间接,但主要是因为我们可以回顾过去的两个步骤,我们可以响应长度为1的列表,而不是长度为0的列表或长度 - {{1}列表。
要完成将n
概括为histo
的最后一步,我们只需用任何代数替换自然投影。因此,我们可以很容易地用dyna
来实现histo
dyna
所以现在我们可以将histo phi = dyna phi project -- project is from the Foldable class
折叠应用于任何甚至可以部分被视为列表的类型(好吧,只要我们继续运行示例并使用histo
作为{{1 },Prim [a]
)。
(从理论上讲,这个代数最终停止有一个限制,例如我们不能处理无限流,但这与理论和优化有关而不是使用。在使用中,这样的事情只需要懒惰,小到足以终止。)
(这反映了通过
Functor
的能力来表示初始代数的想法。如果这真的是一个完全的归纳类型,那么你只能在结束前投射这么多次。)
要从链接的纸张复制加泰罗尼亚数字实例,我们可以创建非空列表
f
并在名为project :: t -> Base t t
的自然数字上创建余代数,并在适当的情况下展开,产生倒计时data NEL a = Some a | More a (NEL a)
data NELf a x = Somef a | Moref a x deriving Functor
natural
然后我们将NEL
式折叠应用于natural :: Int -> NELf Int Int
natural 0 = Somef 0
natural n = Moref n (n-1)
- 自然数的视图,以生成histo
加泰罗尼亚数字。
NELf