Haskell中组织形态的例子

时间:2014-07-22 10:08:07

标签: haskell

我最近阅读了[1]和[2],它们谈到了组织形态(和动力学),它们是递归方案,可以表达例如动态编程。不幸的是,如果您不了解类别理论,那么这些论文是不可访问的,即使其中的代码看起来像Haskell。

有人可以用一个使用真实Haskell代码的例子来解释histomorphisms吗?

  1. Histo- and Dynamorphisms Revisited
  2. Recursion Schemes for Dynamic Programming

2 个答案:

答案 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来看到自己。


我在上面的示例中未显示的是,如果您将类型定义为仿函数的固定点,则catahisto可以非常普遍地实现。我们的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)

我们可以将其视为从catahistodyna的概括连续体。在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