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

时间:2019-01-08 17:06:04

标签: haskell tying-the-knot

上下文

前几天我问了patching a recursively-defined list。我现在正尝试通过在2D列表(列表列表)上进行操作来将其升级。

我将以Pascal三角形为例,例如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>>

(很抱歉用单词将其拼写出来,但是我真的不得不像这样在心理上追踪它,以便第一次理解它。)

出路?

使用我所使用的定义,似乎所有定义都是正确的,在数据​​流方面是明智的。现在看来循环很简单,因为评估程序无法确定所生成的结构是否有限。我找不到一种方法来兑现“无限好”的承诺。

我觉得我需要一些懒惰匹配的话题:一些懒惰的回报,我可以告诉评估者这个(:)的WHNF,但是您仍然需要稍后再调用此thunk来找出答案里面有什么。

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

2 个答案:

答案 0 :(得分:3)

这是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)

从那里,我们可以对其余部分应用补丁/填充功能,以填充第一行和第一列,并编辑任何元素。这些修改将通过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

答案 1 :(得分:1)

为了比较起见,我已经使用@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,但是代码仍然足够繁琐。