组织形态、对称形态和未来形态在列表中的专门应用

42

我最终弄清楚了。请查看我演讲的视频和幻灯片:

原问题:

为了理解通用递归方案(即使用Fix的方案),我发现编写仅针对列表的各种方案很有用。这使得更容易理解实际的方案(没有Fix的额外开销)。

但是,我还没有找到如何定义zygofutu的列表版本。

以下是我目前的专业定义:

cataL :: (a ->        b -> b) -> b -> [a] -> b
cataL f b (a : as) = f a    (cataL f b as)
cataL _ b []       = b

paraL :: (a -> [a] -> b -> b) -> b -> [a] -> b
paraL f b (a : as) = f a as (paraL f b as)
paraL _ b []       = b

-- TODO: histo

-- DONE: zygo (see below)

anaL  :: (b ->       (a, b))               -> b -> [a]
anaL  f b = let (a, b') = f b in a : anaL f b'

anaL' :: (b -> Maybe (a, b))               -> b -> [a]
anaL' f b = case f b of
    Just (a, b') -> a : anaL' f b'
    Nothing      -> []

apoL :: ([b] -> Maybe (a, Either [b] [a])) -> [b] -> [a]
apoL f b = case f b of
    Nothing -> []
    Just (x, Left c)  -> x : apoL f c
    Just (x, Right e) -> x : e

-- DONE: futu (see below)

hyloL  :: (a -> c -> c) -> c -> (b -> Maybe (a, b)) -> b -> c
hyloL f z g = cataL f z . anaL' g

hyloL' :: (a -> c -> c) -> c -> (c -> Maybe (a, c))      -> c
hyloL' f z g = case g z of
    Nothing     -> z
    Just (x,z') -> f x (hyloL' f z' g)

如何为列表定义histozygofutu


你知道 zygofutu 应该有什么类型签名吗? - epsilonhalbe
zygo Fix 版本:zygo :: Functor f => (f b -> b) -> (f (a, b) -> a) -> Fix f -> a - haroldcarr
futu Mu 版本:futu :: (Mu b, Functor (PF b)) => Ann b -> (a -> F b (Futu b a)) -> a -> b(请参见 https://hackage.haskell.org/package/pointless-haskell-0.0.9/docs/Generics-Pointless-RecursionPatterns.html) - haroldcarr
我不知道仅包含列表类型签名 - 仍在努力弄清楚。 - haroldcarr
3个回答

42

Zygomorphism是我们用来描述由两个半互递归函数构建的折叠的高深数学术语。我会举一个例子。

想象一个名为pm :: [Int] -> Int(代表加减)的函数,它通过在数字列表中交替插入+-来实现,使得pm [v,w,x,y,z] = v - (w + (x - (y + z)))。你可以使用原始递归来编写它:

lengthEven :: [a] -> Bool
lengthEven = even . length

pm0 [] = 0
pm0 (x:xs) = if lengthEven xs
             then x - pm0 xs
             else x + pm0 xs

显然,pm0并不具备合成性 - 您需要检查每个位置的整个列表的长度来确定您是在添加还是减去。 Paramorphism模型这种基本递归,当折叠函数需要遍历每次迭代的整个子树时。因此,我们至少可以重写代码以符合已经建立的模式。

paraL :: (a -> [a] -> b -> b) -> b -> [a] -> b
paraL f z [] = z
paraL f z (x:xs) = f x xs (paraL f z xs)

pm1 = paraL (\x xs acc -> if lengthEven xs then x - acc else x + acc) 0

但这种方法效率低下。 lengthEven 在每次参数形态递归迭代时都要遍历整个列表,导致算法的时间复杂度达到 O(n2)。


我们可以注意到,lengthEvenpara 都可以用 foldr 表示为 折叠 函数...

cataL = foldr

lengthEven' = cataL (\_ p -> not p) True
paraL' f z = snd . cataL (\x (xs, acc) -> (x:xs, f x xs acc)) ([], z)

这表明我们可能能够将这两个操作融合为单次列表遍历。

pm2 = snd . cataL (\x (isEven, total) -> (not isEven, if isEven
                                                      then x - total
                                                      else x + total)) (True, 0)

我们有一个fold(折叠)依赖于另一个fold的结果,并且我们能够将它们融合成列表的一次遍历。Zygomorphism恰好捕捉了这个模式。

zygoL :: (a -> b -> b) ->  -- a folding function
         (a -> b -> c -> c) ->  -- a folding function which depends on the result of the other fold
         b -> c ->  -- zeroes for the two folds
         [a] -> c
zygoL f g z e = snd . cataL (\x (p, q) -> (f x p, g x p q)) (z, e)

每次折叠迭代中,f 类似于 catamorphism 看到上一次迭代的答案,但是 g 可以看到两个函数的答案。 gf 相互纠缠。

通过使用第一个折叠函数计算列表长度是奇数还是偶数,使用第二个折叠函数计算总和,我们将 pm 写成一个 zygomorphism。

pm3 = zygoL (\_ p -> not p) (\x isEven total -> if isEven
                                                then x - total
                                                else x + total) True 0
这是经典的函数式编程风格。我们有一个高阶函数负责处理列表,我们只需要插入逻辑来聚合结果。构造显然终止(您只需证明foldr的终止),而且它比原始手写版本更有效率。
另外:@AlexR在评论中指出,zygomorphism有一个名为mutumorphism的大姐姐,它捕捉了所有互递归的荣耀。 mutu概括了zygo,因为两个折叠函数都可以检查另一个迭代的结果。
mutuL :: (a -> b -> c -> b) ->
         (a -> b -> c -> c) ->
         b -> c ->
         [a] -> c
mutuL f g z e = snd . cataL (\x (p, q) -> (f x p q, g x p q)) (z, e)

通过简单地忽略额外的参数,您可以从mutu中恢复出zygo

zygoL f = mutuL (\x p q -> f x p)

当然,所有这些折叠模式都可以从列表推广到任意函子的不动点:

newtype Fix f = Fix { unFix :: f (Fix f) }

cata :: Functor f => (f a -> a) -> Fix f -> a
cata f = f . fmap (cata f) . unFix

para :: Functor f => (f (Fix f, a) -> a) -> Fix f -> a
para f = snd . cata (\x -> (Fix $ fmap fst x, f x))

zygo :: Functor f => (f b -> b) -> (f (b, a) -> a) -> Fix f -> a
zygo f g = snd . cata (\x -> (f $ fmap fst x, g x))

mutu :: Functor f => (f (b, a) -> b) -> (f (b, a) -> a) -> Fix f -> a
mutu f g = snd . cata (\x -> (f x, g x))

zygo的定义与zygoL的定义进行比较。还要注意zygo Fix = para,后三个折叠可以用cata实现。在foldology中,一切都与其他事物相关。

您可以从广义版本中恢复列表版本。

data ListF a r = Nil_ | Cons_ a r deriving Functor
type List a = Fix (ListF a)

zygoL' :: (a -> b -> b) -> (a -> b -> c -> c) -> b -> c -> List a -> c
zygoL' f g z e = zygo k l
    where k Nil_ = z
          k (Cons_ x y) = f x y
          l Nil_ = e
          l (Cons_ x (y, z)) = g x y z

pm4 = zygoL' (\_ p -> not p) (\x isEven total -> if isEven
                                                 then x - total
                                                 else x + total) True 0

我会让其他人回答有关futumorphisms的问题,因为我还没有完全理解它们。 - Benjamin Hodgson
我是否理解正确,zygomorphism是mutumorphism的一个特例? - Alex R
3
@AlexR 是的! mutu 允许每个函数依赖于另一个函数的结果,而 zygo 则允许一个函数依赖于另一个函数的结果。 mutu 推广了 zygo - Benjamin Hodgson
你基于使用情况的推导,让仅限列表的 zygo 更易于理解模式,并清楚了何时应该使用它。谢谢! - haroldcarr
一个小观察。在不再遍历列表以确定偶数/奇数的示例中,“isEven”名称是误导性的。按照惯例,调用是否以“True”或“False”开头(或者可以在调用之前遍历一次列表以确定)。这没什么大不了的——并且不会削弱非常有用的信息。谢谢。 - haroldcarr

14

组织形态学模型 动态规划,这是一种记录先前子计算结果的技术。(有时被称为值课程归纳。)在组织形态学中,折叠函数可以访问早期迭代的折叠结果表格。与分类形态学相比,在分类形态学中,折叠函数只能看到上一次迭代的结果。组织形态学具有后见之明的好处-您可以看到所有历史记录。

这里是想法。当我们消耗输入列表时,折叠代数将输出一系列bhisto将记录每个b,并将其附加到结果表格中。历史记录中的项目数等于您处理的列表层数-当您拆除整个列表时,操作的历史记录长度将等于列表长度。

这就是迭代列表(ory)的历史记录:

data History a b = Ancient b | Age a b (History a b)

“历史”是一系列事物和结果的列表,最后还有一个对应于“[]”事物的额外结果。我们将每个输入列表层与其相应的结果配对。”
cataL = foldr

history :: (a -> History a b -> b) -> b -> [a] -> History a b
history f z = cataL (\x h -> Age x (f x h) h) (Ancient z)

将整个列表从右向左折叠后,最终结果将位于堆栈顶部。

headH :: History a b -> b
headH (Ancient x) = x
headH (Age _ x _) = x

histoL :: (a -> History a b -> b) -> b -> [a] -> b
histoL f z = headH . history f z

偶尔会出现 History a 是一个共函子的情况,但我们只需要headH(原名extract)就可以定义histoL


History 标记每个输入列表的层次结构及其对应的结果。 自由余纯函子 捕捉了标记任意结构的每个层次结构的模式。

data Cofree f a = Cofree { headC :: a, tailC :: f (Cofree f a) }

我通过将 ListF 插入到 Cofree 中并简化来得出了 History
与自由单子相比,可以看出:
data Free f a = Free (f (Free f a))
              | Return a
Free是一个余积类型;Cofree是一个积类型。 Free将一堆f层叠在一起,底部的值为aCofree将一堆a层叠在每个层上。自由单子是广义的外部标记树;余自由共单子是广义的内部标记树。
有了Cofree,我们可以从列表推广到任意函子的不动点,
newtype Fix f = Fix { unFix :: f (Fix f) }

cata :: Functor f => (f b -> b) -> Fix f -> b
cata f = f . fmap (cata f) . unFix

histo :: Functor f => (f (Cofree f b) -> b) -> Fix f -> b
histo f = headC . cata (\x -> Cofree (f x) x)

并再次恢复列表版本。
data ListF a r = Nil_ | Cons_ a r deriving Functor
type List a = Fix (ListF a)
type History' a b = Cofree (ListF a) b

histoL' :: (a -> History' a b -> b) -> b -> List a -> b
histoL' f z = histo g
    where g Nil_ = z
          g (Cons_ x h) = f x h

Aside: histo is the dual of futu. Look at their types.

histo :: Functor f => (f (Cofree f a) -> a) -> (Fix f -> a)
futu  :: Functor f => (a  ->  f (Free f a)) -> (a -> Fix f)

futu is histo with the arrows flipped and with Free replaced by Cofree. Histomorphisms see the past; futumorphisms predict the future. And much like cata f . ana g can be fused into a hylomorphism, histo f . futu g can be fused into a chronomorphism.

即使您跳过数学部分,这篇由Hinze和Wu撰写的论文也提供了一个很好的、以示例为驱动的histomorphisms教程及其用法。


13

由于还没有人回答的问题,我将尝试自己解决。 我将使用。

递归方案中考虑类型: (a -> Base t (Free (Base t) a)) -> a -> t。

我将忽略约束,并将[b]替换为

(a -> Base [b] (Free (Base [b]) a)) -> a -> [b]
(a -> ListF b (Free (ListF b) a)) -> a -> [b]

Free (ListF b) a)是一个列表,可能在末尾有一个类型为a的空位。这意味着它是同构于([b], Maybe a)。所以现在我们有:

(a -> ListF b ([b], Maybe a)) -> a -> [b]

去掉最后一个 ListF,注意到 ListF a b 等同于 Maybe (a, b)

(a -> Maybe (b, ([b], Maybe a))) -> a -> [b]

现在,我非常确定玩类型俄罗斯方块会导致唯一合理的实现方式:
futuL f x = case f x of
  Nothing -> []
  Just (y, (ys, mz)) -> y : (ys ++ fz)
    where fz = case mz of
      Nothing -> []
      Just z -> futuL f z

总结得出的函数是,futuL需要一个种子值和一个可能产生至少一个结果的函数,如果它产生了一个结果,可能会有一个新的种子值。

起初我认为这与

notFutuL :: (a -> ([b], Maybe a)) -> a -> [b]
notFutuL f x = case f x of
  (ys, mx) -> ys ++ case mx of
    Nothing -> []
    Just x' -> notFutuL f x'

实际上,也许在某种程度上是这样的,但唯一显著的区别是真正的futu保证了生产力(即如果f总是返回,则您永远不会因为等待下一个列表元素而被卡住)。


你关于确保生产力的最后一点不是完全正确的。问题比较微妙。在Haskell中,f肯定有可能无限循环或者抛出异常,无法确定。区别在于第二种类型允许f返回一个a(并继续迭代)而不返回任何b。换句话说,如果f终止,则futu将始终终止,而notFutu可以通过终止f来诱使其循环。 - Benjamin Hodgson
1
你的澄清和我的澄清(在括号里)有什么不同?并不是要挑衅,只是好奇。 - Alex R
顺便说一下:notFutuL 的递归调用缺少 f 参数。谢谢! - haroldcarr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接