【问题标题】:List filter using an anamorphism使用变形的列表过滤器
【发布时间】:2013-08-27 15:10:30
【问题描述】:

我使用来自recursion-schemes Hackage 库的变形实现了一个损坏的filter 函数:

import Data.Functor.Foldable

xfilter :: (a -> Bool) -> [a] -> [a]
xfilter f = ana $ project . phi f

phi :: (a -> Bool) -> [a] -> [a]
phi f (h : t) | not (f h) = t
phi f l = l

该函数不是filter 的忠实实现:xfilter odd [1..5] 有效,但xfilter odd [0,0] 无效。我尝试通过在phi 中使用显式递归来实现“重试”,然后用超态重新实现它,所以我以ana . para 结束:

xfilter :: (a -> Bool) -> [a] -> [a]
xfilter f = ana . para $ phi where
    phi Nil = Nil
    phi (Cons h (t, tt)) | f h = Cons h t
    phi (Cons h (t, tt)) = tt

这是令人满意的,但我随后尝试在 phi 中明确表达重试并在外部执行它们:

xfilter :: (a -> Bool) -> [a] -> [a]
xfilter f = ana $ project . retry (phi f)

phi :: (a -> Bool) -> [a] -> Either [a] [a]
phi f (h : t) | not (f h) = Left t
phi f l = Right l

retry f x = case f x of
    Right x -> x
    Left x -> retry f x

Right 表示“产生一个新元素”,Left 表示“使用新种子重试”。

phi 的签名开始看起来与专门用于列表的同构的第一个参数非常相似:

xxapo :: ([a] -> Prim [a] (Either [a] [a])) -> [a] -> [a]
xxapo = apo

[a] -> Either [a] [a][a] -> Prim [a] [a] (Either [a] [a]

所以我想知道是否可以使用同构或其他广义展开来实现过滤,或者ana . para 是我所希望的最好的?

我知道我可以使用折叠,但问题是关于展开的。

【问题讨论】:

  • 我很确定你不能单独使用变形(或变形)来做到这一点。这两种方案表征了生产性核心递归,filter 不一定是生产性的:filter (const False)
  • 我可以使用ana (const Nil)定义filter (const False)
  • 你的filter 应该在无限列表上工作吗?
  • 我现在不在乎。
  • @nponeccop ana- 和 apomorphisms 用于某些函子的余代数。这里我们处理的是列表,所以 colgebra 必须生成一个元素,其余元素的种子,或者一个空列表。由于您需要跳过种子列表中的零个或多个元素,因此您的余代数必然是一个拟态。在您的情况下,这种变形实现了 dropWhile (not.f)

标签: list haskell recursion recursion-schemes corecursion


【解决方案1】:

简而言之:这是不可能的。您总是必须以某种方式分解输入列表,而仅靠展开是无法完成的。您已经可以在代码中看到这一点。你有retry (phi f),相当于dropWhile (not . f),它递归地使用一个输入列表。在您的情况下,递归在 retry 内。

我们可以使用ana 实现filter,但是传递给ana 的函数必须是递归的,如

filter1 :: (a -> Bool) -> [a] -> [a]
filter1 p = ana f
  where
    f [] = Nil
    f (x : xs') | p x       = Cons x xs'
                | otherwise = f xs'

但是,我们可以使用para 实现过滤,而无需任何进一步的递归:

filter2 :: (a -> Bool) -> [a] -> [a]
filter2 p = cata f
  where
    f Nil = []
    f (Cons x r) | p x          = x : r
                 | otherwise    = r

(虽然这不是你一直感兴趣的)。

那么为什么它适用于cata 而不适用于ana

  • Catamorphisms 表示归纳递归,其中每个递归步骤至少消耗一个构造函数。由于每个步骤只需要有限的时间,因此可以确保在使用(有限)数据结构时,整个递归始终终止。
  • 变形表示共归纳递归,其中每个递归步骤都由构造函数保护。这意味着虽然结果可以是无限的,但构建的数据结构的每个部分(构造函数)都是在有限时间内产生的。

现在filter 是如何工作的:在每个步骤中,它都会消耗列表中的一个元素,有时它会产生一个输出元素(如果它满足给定的谓词)。

所以我们看到我们可以将filter 实现为一种变态——我们在有限的时间内消耗列表中的每个元素。

但是我们不能像变形一样实现filter。我们永远无法知道filter 何时产生新结果。我们不能仅使用有限数量的操作来描述下一个输出元素的产生。例如,让我们采用filter odd (replicate n 0 ++ [1]) - 它需要 O(n) 个步骤来生成第一个元素 1。所以必须有某种递归搜索输入列表,直到找到满意的元素。

【讨论】:

  • 很好的解释! “你总是必须分解输入列表”是一个很好的论点。
【解决方案2】:
    xfilter :: (a -> Bool) -> [a] -> [a]
    xfilter f xs = last $ apo phi ([xs], []) where
        phi ([[]], ys) = Cons [] $ Left [ys]
        phi ([h:t], ys) | f h = Cons [] $ Right ([t], h:ys)
        phi ([h:t], ys) = Cons [] $ Right ([t], ys)

但最后是一个目录。

【讨论】:

  • 除了 last 是一个 cata,我们最终通过种子完成所有列表构建,绕过展开的元素发射。 xfilter,如此处所写,将反转列表,但这可能会通过 dlist 技巧来解决。
  • @duplode 将filter 转化为高效的 corecursion 的技巧,我在一些关于 corecursion 和(还有什么)埃拉托色尼筛子的论文中看到,就是让每一步都产生一个 list 最多一个元素,并通过一个 concat 传递整体结果(将递归责任移到 it 上)。 concat 当然是递归的,因为它不知道要跳过多少 [],但它比这里的 reverse . last 离线少得多。所以,afilter p = concat . ana phi where phi [] = Nil ; phi (x:xs) = Cons [x | p x] xs,我猜。
  • @WillNess 这很简洁——一个带有虚拟列表和折叠侧的concat(或catMaybes)的hylo。
  • 是的,catMaybes 要好得多——使用 MonadComprehensions,phi 的代码甚至保持不变。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-02-08
  • 2023-03-23
  • 1970-01-01
  • 1970-01-01
  • 2021-12-05
相关资源
最近更新 更多