根据所有先前的术语计算列表的术语

Computing a term of a list depending on all previous terms

我有以下恒等式,它(隐含地)定义了正整数的分区数(也就是说,您可以将整数写为有序非零正整数之和的方式的数量):

一些注意事项:

  1. Flajolet 和 Sedjewick 在 Analytic Combinatorics 一书中对此进行了研究,公式的图像取自那里,因为 Whosebug 不支持 LaTeX。

  2. sigma 是一个数的除数之和

我想编写一个 haskell 程序来计算具有 P 系数的列表。 第 i 个项取决于所有之前的项(是压缩 sigma 和之前的 Ps 得到的列表的总和)。 这个问题是一个很好的例子,说明如何根据规范 "calculate" 程序,就像 Gibbons 在他的 paper.

中写的那样

问题是:是否存在捕获此类计算的已知递归方案?列表中的每一项都依赖于所有之前的项的计算,(结果与之前的没有关系,我的意思是,你必须对每一项进行新的遍历)

使用自引用和惰性怎么样?

假设 σ 的值在无限列表 sigma 中,则

p = [sum (zipWith (*) sigmas (reverse ps)) | ps <- inits p]

会非常巧妙地实现这个递归。

我在这里忽略 n 的因素,为了代码的简单性,也因为我不确定 P_0 应该是什么。

法定微积分警告。这个问题的基本答案涉及专门化标准递归方案。但是我有点忘乎所以地扯着它的线。当我试图将相同的方法应用于列表以外的结构时,事情发生了更抽象的转变。我最终找到了 Isaac Newton 和 Ralph Fox,并在此过程中设计了 alopegmorphism,这可能是新事物。

但是无论如何,应该存在这样的东西。它看起来像是 变形 或 "unfold" 的特例。先从库里那个叫unfoldr

说起吧
unfoldr :: (seed -> Maybe (value, seed)) -> seed -> [value]

它展示了如何从种子生成值列表,重复使用称为 coalgebra 的函数。在每一步,余代数都会说是停止 [] 还是通过将一个值添加到从新种子生成的列表中来继续。

unfoldr coalg s = case coalg s of
  Nothing       -> []
  Just (v, s')  -> v : unfoldr coalg s'

在这里,seed 类型可以是任何你喜欢的类型——任何适合展开过程的局部状态。一个完全明智的种子概念就是 "the list so far",也许是相反的顺序,以便最近添加的元素最近。

growList :: ([value] -> Maybe value) -> [value]
growList g = unfoldr coalg B0 where
  coalg vz = case g vz of   -- I say "vz", not "vs" to remember it's reversed
    Nothing  -> Nothing
    Just v   -> Just (v, v : vz)

在每一步,我们的 g 操作都会查看我们已经拥有的值的上下文,并决定是否添加另一个:如果是,新值将同时成为列表的头部和最新值在新的背景下。

因此,此 growList 会在每个步骤中为您提供先前结果的列表,为 zipWith (*) 做好准备。反转对于卷积来说相当方便,所以也许我们正在寻找类似

的东西
ps = growList $ \ pz -> Just (sum (zipWith (*) sigmas pz) `div` (length pz + 1))
sigmas = [sigma j | j <- [1..]]

也许吧?

递归方案?对于列表,我们有变形的特例,其中种子是我们迄今为止构建的内容的上下文,一旦我们'我们已经说过如何构建更多内容,我们知道如何通过相同的方式扩展上下文。不难看出这对列表是如何起作用的。但它一般如何用于变形镜头? 这就是问题所在。

我们建立了可能无穷大的值,其节点形状由某个函子 f 给出(当我们 "tie the knot" 时,其参数结果为 "substructures")。

newtype Nu f = In (f (Nu f))

在变形中,余代数使用种子为最外层节点选择一个形状,填充子结构的种子。 (共同)递归地,我们映射变形,将这些种子生长到子结构中。

ana :: Functor f => (seed -> f seed) -> seed -> Nu f
ana coalg s = In (fmap (ana coalg) (coalg s))

让我们从 ana 重构 unfoldr。我们可以从 Nu 和一些简单的部分构建许多普通的递归结构:多项式函子套件.

newtype  K1 a      x  = K1 a                  -- constants (labels)
newtype  I         x  = I x                   -- substructure places
data     (f :+: g) x  = L1 (f x) | R1 (g x)   -- choice (like Either)
data     (f :*: g) x  = f x :*: g x           -- pairing (like (,))

具有 Functor 个实例

instance Functor (K1 a) where fmap f (K1 a) = K1 a
instance Functor I      where fmap f (I s) = I (f s)
instance (Functor f, Functor g) => Functor (f :+: g) where
  fmap h (L1 fs) = L1 (fmap h fs)
  fmap h (R1 gs) = R1 (fmap h gs)
instance (Functor f, Functor g) => Functor (f :*: g) where
  fmap h (fx :*: gx) = fmap h fx :*: fmap h gx

对于 value 的列表,节点形状仿函数是

type ListF value = K1 () :+: (K1 value :*: I)

意思是"either a boring label (for nil) or the (cons) pair of a value label and a sublist"。 ListF value 余数的类型变为

seed -> (K1 () :+: (K1 value :*: I)) seed

同构(通过 "evaluating" 多项式 ListF valueseed)到

seed -> Either () (value, seed)

距离

仅咫尺之遥
seed -> Maybe (value, seed)

unfoldr 所期望的。你可以像这样恢复一个普通的列表

list :: Nu (ListF a) -> [a]
list (In (L1 _))                = []
list (In (R1 (K1 a :*: I as)))  = a : list as

现在,我们如何种植一些通用的 Nu f?一个好的开始是为最外层节点选择 shapef () 类型的值仅给出节点的形状,在子结构位置上有一些琐碎的存根。事实上,为了种植我们的树,我们基本上需要一些方法来选择 "next" 节点形状,给出我们已经到达的位置和我们到目前为止所做的事情的一些想法。我们应该期待

grow :: (..where I am in a Nu f under construction.. -> f ()) -> Nu f

请注意,对于不断增长的列表,我们的阶梯函数 returns a ListF value ()Maybe value.

同构

但是我们如何表达目前我们在 Nu f 中的位置呢?我们将从结构的根开始 so-many-nodes-in,所以我们应该期待一堆层。每一层都应该告诉我们(1)它的形状,(2)我们当前所在的位置,以及(3)已经在该位置左侧构建的结构,但我们希望在我们所在的位置仍然有存根还没有到。换句话说,它是我 2008 年关于 Clowns and Jokers.

的 POPL 论文中 dissection 结构的一个示例

解剖运算符将函子 f(视为元素的容器)转换为具有两种不同元素的双函子 Diss f,左边的元素(小丑)和右边的元素f 结构中 "cursor position" 的右(小丑)。首先,让我们有 Bifunctor class 和一些实例。

class Bifunctor b where
  bimap :: (c -> c') -> (j -> j') -> b c j -> b c' j'

newtype K2 a       c j = K2 a
data    (f :++: g) c j = L2 (f c j) | R2 (g c j)
data    (f :**: g) c j = f c j :**: g c j
newtype Clowns f   c j = Clowns (f c)
newtype Jokers f   c j = Jokers (f j)

instance Bifunctor (K2 a) where
  bimap h k (K2 a) = K2 a
instance (Bifunctor f, Bifunctor g) => Bifunctor (f :++: g) where
  bimap h k (L2 fcj) = L2 (bimap h k fcj)
  bimap h k (R2 gcj) = R2 (bimap h k gcj)
instance (Bifunctor f, Bifunctor g) => Bifunctor (f :**: g) where
  bimap h k (fcj :**: gcj) = bimap h k fcj :**: bimap h k gcj
instance Functor f => Bifunctor (Clowns f) where
  bimap h k (Clowns fc) = Clowns (fmap h fc)
instance Functor f => Bifunctor (Jokers f) where
  bimap h k (Jokers fj) = Jokers (fmap k fj)

请注意 Clowns f 是一个双函子,相当于一个只包含小丑的 f 结构,而 Jokers f 只有小丑。如果您对重复所有 Functor 工具来获得 Bifunctor 工具感到困扰,那么您是对的:如果我们抽象出元数并在 indexed 集,但那是另外一回事了。

让我们将 dissection 定义为 class,它将双函子与函子相关联。

class (Functor f, Bifunctor (Diss f)) => Dissectable f where
  type Diss f :: * -> * -> *
  rightward   ::  Either (f j) (Diss f c j, c) ->
                  Either (j, Diss f c j) (f c)

代表类型Diss f c j在一个元素位置输入一个 f 结构,其中一个 "hole" 或 "cursor position",并且在孔左侧的位置我们在 c 中有 "clowns" ,右边 j 中有 "jokers"。 (该术语摘自 Stealer's Wheel 歌曲 "Stuck in the Middle with You"。)

class中的关键操作是同构rightward,它告诉我们如何向右移动一个位置,从

开始
  • 在充满小丑的整个结构的左侧,或
  • 结构上有一个洞,还有一个小丑要放进洞里

并到达

  • 结构上的一个洞,以及从中钻出来的小丑,或者
  • 整个结构的右边都是小丑。

艾萨克牛顿喜欢解剖,但他称它们为分差,并在real-valued函数上定义它们以获得曲线上两点之间的斜率,因此

divDiff f c j  =  (f c - f j) / (c - j)

他用它们对任何旧函数进行最佳多项式逼近,等等。乘以乘出

divDiff f c j * c - j * divDiff f c j  =  f c - f j

然后两边相加去掉减法

f j + divDiff f c j * c  =  f c + j * divDiff f c j

你得到了 rightward 同构。

如果我们查看实例,我们可能会对这些事情建立更多的直觉,然后我们可以回到原来的问题。

一个无聊的旧常量的差分值为零。

instance Dissectable (K1 a) where
  type Diss (K1 a) = K2 Void
  rightward (Left (K1 a)) = (Right (K1 a))
  rightward (Right (K2 v, _)) = absurd v

如果我们从左边开始到右边,我们会跳过整个结构,因为没有元素位置。如果我们从元素位置开始,有人在撒谎!

恒等函子只有一个位置。

instance Dissectable I where
  type Diss I = K2 ()
  rightward (Left (I j))       = Left (j, K2 ())
  rightward (Right (K2 (), c)) = Right (I c)

如果我们从左边开始,我们就会到达位置,然后弹出小丑;推一个小丑,我们在右边完成。

对于总和,结构是继承的:我们只需要正确地取消标记和重新标记。

instance (Dissectable f, Dissectable g) => Dissectable (f :+: g) where
  type Diss (f :+: g) = Diss f :++: Diss g
  rightward x = case x of
      Left (L1 fj)      -> ll (rightward (Left fj))
      Right (L2 df, c)  -> ll (rightward (Right (df, c)))
      Left (R1 gj)      -> rr (rightward (Left gj))
      Right (R2 dg, c)  -> rr (rightward (Right (dg, c)))
    where
      ll (Left (j, df)) = Left (j, L2 df)
      ll (Right fc)     = Right (L1 fc)
      rr (Left (j, dg)) = Left (j, R2 dg)
      rr (Right gc)     = Right (R1 gc)

对于产品,我们必须在一对结构中的某个位置:要么我们在小丑和小丑之间的左边,右边的结构都是小丑,要么左边的结构都是小丑,我们在右边在小丑和小丑之间。

instance (Dissectable f, Dissectable g) => Dissectable (f :*: g) where
  type Diss (f :*: g) = (Diss f :**: Jokers g) :++: (Clowns f :**: Diss g)
  rightward x = case x of
      Left (fj :*: gj) -> ll (rightward (Left fj)) gj
      Right (L2 (df :**: Jokers gj), c) -> ll (rightward (Right (df, c))) gj
      Right (R2 (Clowns fc :**: dg), c) -> rr fc (rightward (Right (dg, c)))
    where
      ll (Left (j, df)) gj = Left (j, L2 (df :**: Jokers gj))
      ll (Right fc)     gj = rr fc (rightward (Left gj))  -- (!)
      rr fc (Left (j, dg)) = Left (j, R2 (Clowns fc :**: dg))
      rr fc (Right gc)     = Right (fc :*: gc)

rightward 逻辑确保我们从左边的结构开始工作,然后一旦我们完成它,我们就开始右边的工作。标记为(!)的线是中间的关键时刻,我们从左侧结构的右侧出来,然后进入右侧结构的左侧。

Huet 的 "left" 和 "right" 数据结构中游标移动的概念源于可分解性(如果您完成 rightward 与其 leftward 对应物的同构)。 f 导数 只是当小丑和小丑之间的差异趋于零时的极限,或者对我们来说,当你有相同类型的东西时你得到的东西光标。

此外,如果你把小丑数设为零,你会得到

rightward :: Either (f x) (Diss f Void x, Void) -> Either (x, Diss f Void x) (f Void)

但我们可以去掉不可能输入的情况得到

type Quotient f x = Diss f Void x
leftmost :: f x -> Either (x, Quotient f x) (f Void)
leftmost = rightward . Left

这告诉我们每个 f 结构要么有一个最左边的元素,要么根本没有 none,这是我们在学校学习的结果 "Remainder Theorem"。 Quotient 运算符的多元版本是 Brzozowski 应用于正则表达式的 "derivative"。

但是我们的特例是Fox的导数(我从Dan Piponi那里学到的):

type Fox f x = Diss f x ()

这是 f 结构的类型,游标右侧有存根。 现在我们可以给出通用grow运算符的类型。

grow :: Dissectable f => ([Fox f (Nu f)] -> f ()) -> Nu f

我们的"context"是一堆层,每个层的左边都有完全增长的数据,右边有存根。我们可以直接实现grow如下:

grow g = go [] where
  go stk = In (walk (rightward (Left (g stk)))) where
    walk (Left ((), df)) = walk (rightward (Right (df, go (df : stk))))
    walk (Right fm)      = fm

当我们到达每个位置时,我们提取的小丑只是一个存根,但它的上下文告诉我们如何扩展堆栈以生成树的子结构,这为我们提供了我们需要的小丑向右移。一旦我们用树填充了所有存根,我们就完成了!

但这里有一个转折点:grow 并不是那么容易表达为变形。很容易为每个节点的最左边的 child 提供 "seed",因为我们只有右边的存根。但是为了给右边的下一个种子,我们需要的不仅仅是最左边的种子——我们需要从它长出来的树!变形模式要求我们在生长任何子结构之前为子结构提供所有种子。我们的 growList 是变形只是因为列表节点有 至多一个 child.

所以它毕竟是一种新事物,从无到有,但允许给定层的后期增长依赖于较早的树,Fox 派生体抓住了 "stubs where we have yet to work" 的想法。也许我们应该称它为 alopegmorphism,来自希腊语 αλωπηξ,表示 "fox".