【问题标题】:Examples of histomorphisms in HaskellHaskell 中的组织同态示例
【发布时间】:2014-07-27 16:38:03
【问题描述】:

我最近阅读了 [1] 和 [2],它们谈到了组织同态(和动态同态),它们是递归方案,可以表达例如动态规划。不幸的是,如果您不了解范畴论,则无法访问这些论文,即使其中有看起来像 Haskell 的代码。

有人能用一个使用真实 Haskell 代码的例子来解释组织同态吗?

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

【问题讨论】:

  • 你见过pointless-haskell吗?它有组织同态和动态同态的例子,尽管组织同态的唯一例子是斐波那契数列。也许有人可以用这些例子来解释 -morphisms。
  • @Zeta 我有,但它传达的想法和论文一样清楚:histo :: (Mu a,Functor (PF a)) => Ann a -> (F a (Histo a c) -> c) -> a -> c
  • recursion-schemeshistodyna 只是histo,适用于任何代数,而不仅仅是最终的。我能想到的最简单有趣的例子是一个相当直接的tail,它建立在一个通用的foldr :: (a -> b -> b -> b) -> (a -> b -> b) -> b -> [a] -> b

标签: haskell


【解决方案1】:

让我们从定义一个我们将用作示例的数据类型开始:

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 的函数。

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

我们可以使用这种变质将Nats 转换回Integers,如下所示:

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,我们可以访问直接子项的值。但是假设我们也喜欢访问传递子项的值,例如,在定义斐波那契函数时。然后,我们不仅需要访问前一个值,还需要访问第二个前一个值。这就是组织同态发挥作用的地方。

组织同构(histo 听起来很像“历史”)允许我们访问所有以前的值,而不仅仅是最近的值。这意味着我们现在得到一个值列表,而不仅仅是一个值,所以组织同构的类型是:

-- 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 的情况下可能更有意义)。所以你基本上可以看到已经应用的“减少序列”(=历史)。

【讨论】:

  • cata-、histo-、ana-、apo- 和 hylomorphisms 在 recursion-schemes 包中实现。缺少文档,但是通过将仿函数定义为固定点(Mu 类型、Nu 类型或直接)或提供 Base 仿函数和项目/嵌入,您可以获得具有许多您想要的态射的可折叠/可折叠实例。
  • 那么你可以通过对余代数的选择不那么严格来获得dynamorphism:dyna :: Functor f =&gt; (f (Cofree f a) -&gt; a) -&gt; (c -&gt; f c) -&gt; (c -&gt; a)。这里c 替换Fix f(c -&gt; f c)recursion-schemes'project 的限制较少的形式。
  • 我没想到catahisto 会同样强大。 dyna 那么强大吗?
  • @J.Abrahamson 不是 dyna 只是手动传递 Foldable 字典吗?所以 dyna 是 histo 就像 sortBy 是排序?
  • @tibbe 我怀疑 histo 和 cata 在较弱/较大的分类设置中并不同样强大。我没有尝试使用cata 实现histo,但我怀疑它需要strength
【解决方案2】:

我们可以认为这是从catahisto 再到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 通过让对任何类型t 进行操作来改进histo,只要我们可以为它制作一个f-coalgebra,而不仅仅是Foldable 那些(它有通用的Base t-coalgebras 作为Foldable 见证数据类型是最终的coalgebras)。

我们几乎可以通过简单地查看实现它们的类型需要什么来读取它们的属性。

例如,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 我们应该能够对它做同样的事情。这是一个基于histofoldr

foldr :: (a -> b -> b) -> b -> [a] -> b
foldr cons nil = histo $ \case
  Nil             -> nil
  Cons a (b :< _) -> cons a b

我们可以看到,我们不再立即获得前一个折叠结果,而是必须进入Cofree 的第一层才能找到它。但是Cofree 是一个流,可能包含无限多的“先前折叠值”,我们可以尽可能深入地挖掘它。这就是赋予histo“历史”力量的原因。例如,我们可以使用histo 编写一个相当直接的tail,这比单独使用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 的列表或长度为 -n 的列表不同.

为了将histo 推广到dyna 的最后一步,我们只需将自然投影替换为any 余代数。因此,我们可以很容易地根据dyna 实现histo

histo phi = dyna phi project -- project is from the Foldable class

所以现在我们可以将histo 折叠应用于任何甚至可以部分视为列表的类型(好吧,只要我们继续运行示例并将Prim [a] 用作Functorf )。

(理论上,这个余数最终停止有一个限制,例如我们不能处理无限流,但这更多地与理论和优化有关,而不是使用。在使用中,这样的事情只是必须足够懒惰并且足够小才能终止。)

(这反映了通过project :: t -&gt; 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 视图,以生成n-th 加泰罗尼亚数。

-- here's a quick implementation of `dyna` using `recursion-schemes`

zcata
  :: (Comonad w, Functor f) =>
     (a -> f a) -> (f (w (w c)) -> w b) -> (b -> c) -> a -> c
zcata z k g = g . extract . c where
  c = k . fmap (duplicate . fmap g . c) . z

dyna :: Functor f => (f (Cofree f c) -> c) -> (a -> f a) -> a -> c
dyna phi z = zcata z distHisto phi

takeC :: Int -> Cofree (NELf a) a -> [a]
takeC 0 _                 = []
takeC n (a :< Somef v)    = [a]
takeC n (a :< Moref v as) = a : takeC (n-1) as

catalan :: Int -> Int
catalan = dyna phi natural where
  phi :: NELf Int (Cofree (NELf Int) Int) -> Int
  phi (Somef 0) = 1
  phi (Moref n table) = sum (zipWith (*) xs (reverse xs))
    where xs = takeC n table

【讨论】:

  • BasePrim 的用途是什么。我看到它们是在递归方案中定义的,但是它们在其中定义的模块缺少任何文档。为什么递归方案定义了一个不同于常用的Foldable 类?该类中的project 函数有什么作用?
  • Base 是一个类型族,其中Base tt 类型的签名函子,即f 代数中的ft 是最终/的初始 co/alebgra。 Foldable 这里是Data.Foldable 的广义形式——您可以将Data.Foldable 视为Data.Functor.Foldable 专门化为Base (t a) = Base [a]。最后,project 基本上是 uncons :: [a] -&gt; Maybe (a, [a]),但再次泛化为任意的 Base 函子。
  • 哦!由于Base 只是一个类型族,Prim 是一个数据族,它可以让您避免为ListSignatureMaybeSignatureBoolSignatureNatSignature 等数据类型创建新名称。同时拥有BasePrim 只是方便。
  • 使用 dyna 获取加泰罗尼亚数字而不是直接使用 histo 有什么意义?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-03-04
  • 1970-01-01
  • 2012-08-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多