双递归地定义列表的双重无限列表

Birecursively defining a doubly infinite list of lists

上下文

前几天我问了。我现在正尝试通过在 2D 列表(列表的列表)上操作来提高它的水平。

我将以帕斯卡的三角形为例,例如 this beautiful one:

pascals = repeat 1 : map (scanl1 (+)) pascals
[1,1,1,1,1,1...
[1,2,3,4,5...
[1,3,6,10...
[1,4,10...
[1,5...
[1...

问题

我想这样表达:

  1. 我会用我自己的第一行和第一列(上面的例子假设第一行是 repeat 1,这是可以修复的,第一列是 repeat (head (head pascals)),这会更棘手)

  2. 每个元素仍然是上一个和左一个的函数。

  3. 作为一个整体,它本身就是一个函数,足以在定义中插入一个补丁函数并让它传播补丁。

所以从外部来看,我想找到一个 f 函数,以便我可以这样定义 pascal

pascal p = p (f pascal)

...因此 pascal id 与示例中的相同,并且 pascal (patch (1,3) to 16) 产生如下内容:

[1,1,1,1, 1,1...
[1,2,3,16,17...
[1,3,6,22...
[1,4,10...
[1,5...
[1...

我在哪里

让我们首先定义并提取第一行和第一列,这样我们就可以使用它们,而不是想滥用它们的内容。

element0 = 1
row0 = element0 : repeat 1
col0 = element0 : repeat 1

更新定义以使用 row0 非常简单:

pascals = row0 : map (scanl1 (+)) pascals

但第一列仍然是element0。正在更新以从 col0:

获取它们
pascals = row0 : zipWith newRow (tail col0) pascals
  where
    newRow leftMost prevRow = scanl (+) leftMost (tail prevRow)

现在我们已经满足了第一个要求(自定义第一行和第一列)。没有打补丁,第二个还是不错的

我们甚至得到了第三部分:如果我们修补一个元素,它将向下传播,因为 newRow 是根据 prevRow 定义的。但它不会向右传播,因为 (+)scanl 的内部累加器上运行,并且来自 leftMost,这在这种情况下是显式的。

我试过的

由此看来,正确的做法似乎是真正分离关注点。我们希望我们的初始化器 row0col0 在定义中尽可能明确,并找到一种独立定义矩阵其余部分的方法。存根:

pascals = row0 : zipWith (:) (tail col0) remainder
[1,1,1,1,1,1,1,1,1,1...
[1,/-------------------
[1,|
[1,|
[1,|
[1,|  remainder
[1,|
[1,|
[1,|
[1,|

然后我们希望根据整体直接定义余数。自然的定义是:

remainder = zipWith genRow pascals (tail pascals)
  where genRow prev cur = zipWith (+) (tail prev) cur
[1,1,1,1,1,1,1,1,1,1...
<<loop>>

第一行很好。为什么循环?以下评估有帮助:pascals 被定义为缺点,他的车很好(并打印出来)。 cdr是什么?是 zipWith (:) (tail col0) remainder。该表达式是 [] 还是 (:)?它是其参数 tail col0remainder 中最短的一个。 col0 是无限的,它与 remainder zipWith genRow pascals (tail pascals) 一样为空。那是 [] 还是 (:)?好吧,pascals 已经被评估为 (:),但是 (tail pascals) 还没有找到 WHNF。我们已经在尝试了,所以 <<loop>>.

(很抱歉用文字拼写出来,但我真的不得不像那样在脑海中追踪它才能第一次理解它)。

出路?

根据我的定义,似乎所有定义都是正确的,数据流明智的。循环现在看起来很简单,因为评估者无法决定生成的结构是否有限。我找不到让它成为承诺的方法 "it's infinite all right"。

我觉得我需要一些惰性匹配的反面:一些惰性返回,我可以告诉评估者 WHNF 的结果是 (:),但是你稍后仍然需要调用这个 thunk 来看看里面有什么。

它仍然感觉像一个固定点,但我还没有设法以一种有效的方式表达。

这里有一个更懒惰的 zipWith 版本,可以使您的示例富有成效。它假定第二个列表至少与第一个列表一样长,而不强制它。

zipWith' :: (a -> b -> c) -> [a] -> [b] -> [c]
zipWith' f (i : is) ~(j : js) = f i j : zipWith' f is js

-- equivalently --

zipWith' f (i : is) jjs = f i (head j) : zipWith' f is (tail js)

查看我们要定义的矩阵:

matrix =
  [1,1,1,1,1,1,1...
  [1,/-------------
  [1,|
  [1,|  remainder
  [1,|
  ...

矩阵和余数之间有一个简单的关系,它描述了这样一个事实,即余数中的每个条目都是通过将其左侧的条目与其上方的条目相加而获得的:取矩阵的总和,但不包括它第一行,以及没有第一列的矩阵。

remainder = (zipWith . zipWith) (+) (tail matrix) (map tail matrix)

从那里,我们可以对余数应用 patch/padding 函数,以填充第一行和第一列,并编辑任何元素。这些修改将通过 matrix 的递归出现反馈回来。这导致以下 pascals 的广义定义:

-- parameterized by the patch
-- and the operation to generate each entry from its older neighbors
pascals_ :: ([[a]] -> [[a]]) -> (a -> a -> a) -> [[a]]
pascals_ pad (+) = self where
  self = pad ((zipWith . zipWith) (+) (tail self) (map tail self))

例如,最简单的填充函数就是用初始的行和列来完成矩阵。

rowCol :: [a] -> [a] -> [[a]] -> [[a]]
rowCol row col remainder = row : zipWith' (:) col remainder

这里我们必须注意在余数中偷懒,因为我们正在定义它,因此使用上面定义的zipWith'。换句话说,我们必须确保如果我们将 undefined 传递给 rowCol row col,我们仍然可以看到可以生成矩阵其余部分的初始值。

现在pascals可以定义如下

pascals :: [[Integer]]
pascals = pascals_ (rowCol (repeat 1) (repeat 1)) (+)

截断无限矩阵的助手:

trunc :: [[Integer]] -> [[Integer]]
trunc = map (take 10) . take 10

为了比较,我按照@luqui 的建议使用 Data.IntTrie 编写了一个替代版本。

pascal :: Trie2D Int
pascal = overwriteRow 0 1 $ overwriteCol 0 1 $
         liftA2 (+) (shiftDown pascal) (shiftRight pascal)

使用以下 Trie2D 结构:

newtype Trie2D a = T2 { unT2 :: IntTrie (IntTrie a) }

instance Functor Trie2D where
  fmap f (T2 t) = T2 (fmap f <$> t)

instance Applicative Trie2D where
  pure = T2 . pure . pure
  ~(T2 f) <*> ~(T2 a) = T2 $ (<*>) <$> f <*> a -- took some head-scratching

apply2d :: Trie2D a -> Int -> Int -> a
apply2d (T2 t) i j = t `apply` i `apply` j

并支持代码:

overwriteRow,overwriteCol :: Int -> a -> Trie2D a -> Trie2D a
overwriteRow i x = T2 . overwrite i (pure x) . unT2
overwriteCol j x = T2 . fmap (overwrite j x) . unT2

shiftUp, shiftDown, shiftLeft, shiftRight :: Trie2D a -> Trie2D a
shiftUp    (T2 t) = T2 (shiftL t)
shiftDown  (T2 t) = T2 (shiftR t)
shiftLeft  (T2 t) = T2 (shiftL <$> t)
shiftRight (T2 t) = T2 (shiftR <$> t)

shiftL, shiftR :: IntTrie a -> IntTrie a
shiftL t = apply t . succ @Int <$> identity
shiftR t = apply t . pred @Int <$> identity

t2dump :: Show a => Trie2D a -> IO ()
t2dump t2 = mapM_ print [ [ apply2d t2 i j | j <- [0..9] ] | i <- [0..9] ]

别忘了补丁功能,它整个问题的根本原因:

overwrite2d :: Int -> Int -> a -> Trie2D a -> Trie2D a
overwrite2d i j x = T2 . modify i (overwrite j x) . unT2

花了一点时间,但结果非常令人满意。感谢您给我机会尝试这个!

一旦支持代码启动,运行。

,我确实很享受写作的轻松

欢迎评论!请原谅我将 Bits 实例强行设置为 Int 很多,但代码本身已经够毛茸茸的了。