【问题标题】:Birecursively defining a doubly infinite list of lists双递归定义列表的双重无限列表
【发布时间】:2019-06-03 10:12:16
【问题描述】:

上下文

前几天我询问了patching a recursively-defined list。我现在正试图通过对 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。我们已经在尝试了,所以&lt;&lt;loop&gt;&gt;

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

出路?

根据我的定义,似乎所有定义都是正确的,数据流明智的。循环现在看起来很简单,因为评估器无法确定生成的结构是否是有限的。我找不到办法让它成为“它是无限的”的承诺。

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

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

【问题讨论】:

  • 所以,和上次一样,我的问题更复杂(虽然没有上次那么多),但这是我目前最少的障碍点。我什至没有阻止编写补丁功能,我什至不能编写生成器。我的初始化器既不是 1 也不是可比的;我的操作不是(+)
  • "评估者无法确定生成的结构是否是有限的[...]" 评估者不关心;它只会评估您告诉它的内容。如果有一个循环,你已经在某个地方自我引用了。
  • 如果您更多地使用多维无限递归结构,您可能希望从列表转向trie,以获得更好的性能和更少的尴尬。
  • 我已编辑我的答案以回答您的主要问题。 (以防您错过通知。)
  • @AJFarmar:这既无用也无建设性。我知道为什么会有一个循环。我在您引用的内容上方详细说明了一段。我知道评估员不在乎。或者有什么别的感觉,FTM。我觉得整个前面的部分都应该清楚地表明我对评估的工作方式有一个模糊的概念;因此,如果这里不够明显,则将其拼写出来:该句子既不是没有上下文的答案,也不是没有上下文。

标签: haskell tying-the-knot


【解决方案1】:

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

【讨论】:

  • 这太完美了。太感谢了!我将假设以下 cmets:1)感谢您在写问题时非常想念的“富有成效”这个词; 2) 我喜欢你将(zipWith . zipWith) 表达式称为“简单” :-) 这需要我一些时间,但我还没有调用直观(但我一直在写(f .) . (+)-like 表达式,看中了); 3) 讽刺地称它为zipWith',考虑到foldl/foldl' 的情况; 4)所以没有办法用标准库做到这一点?
  • 对,我不知道有什么常见的地方可以导入zipWith'。你应该试试zipWith . zipWith . zipWith :) 我认为这些东西的类型在这里很有指示性。
【解决方案2】:

为了比较起见,我按照@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 很多,但代码已经够麻烦了。

【讨论】:

    猜你喜欢
    • 2014-06-05
    • 1970-01-01
    • 1970-01-01
    • 2015-04-19
    • 1970-01-01
    • 2013-07-18
    • 1970-01-01
    • 2018-02-09
    • 1970-01-01
    相关资源
    最近更新 更多