Birecursively defining a doubly infinite list of lists
Asked Answered
R

2

3

Context

I asked about patching a recursively-defined list the other day. I'm now trying to bring it up a level by operating on a 2D list instead (a list of lists).

I'll use Pascal's triangle as an example, like for example 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...

Question

I'd like to express it such that:

  1. I'll come with my own first rows and columns (example above assumes first row is repeat 1, which is fixable enough, and that first column is repeat (head (head pascals)), which is going to be more tricky)

  2. Each element remains a function of the one above and the one left of it.

  3. As a whole, it is a function of itself enough for it to be possible to insert a patching function in the definition and have it propagate patches.

So from the outside, I'd like to find an f function such that I can define pascal as such:

pascal p = p (f pascal)

...so that pascal id is the same as in the example, and pascal (patch (1,3) to 16) yields something like:

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

Where I'm at

Let's first define and extract the first row and column, so we can have them available and not be tempted to abuse their contents.

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

Updating the definition to use row0 is easy enough:

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

But the first column is still element0. Updating to take them from col0:

pascals = row0 : zipWith newRow (tail col0) pascals
  where
    newRow leftMost prevRow = scanl (+) leftMost (tail prevRow)

Now we're good with the first requirement (custom first row and column). With no patching, the second is still good.

We even get part of the third: if we patch an element, it will propagate downwards since newRow is defined in terms of prevRow. But it won't propagate rightwards, since the (+) operates on scanl's internal accumulator, and from leftMost, which is an explicit in this context.

What I've tried

From there, it seems like the right way to do is to really separate concerns. We want our initializers row0 and col0 as explicit as possible in the definition, and find a way to define the rest of the matrix independently. Stub:

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,|

and then we'd want the remainder defined directly in terms of the whole. The natural definition would be:

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

The first row comes out fine. Why the loop? Following the evaluation helps: pascals is defined as a cons, whose car is fine (and printed). What's is cdr? It's zipWith (:) (tail col0) remainder. Is that expression a [] or (:)? It's the shortest of its arguments tail col0 and remainder. col0 being infinite, it's as null as remainder, i.e. zipWith genRow pascals (tail pascals). Is that [] or (:)? Well, pascals has already been evaluated to (:), but (tail pascals) hasn't been found a WHNF yet. And we're already in the process of trying, so <<loop>>.

(Sorry for spelling it out with words, but I really had to mentally trace it like that to understand it the first time).

Way out?

With the definitions I'm at, it seems like all definitions are proper, data-flow wise. The loop now seems simply because the evaluator can't decide whether the generated structure is finite or not. I can't find a way to make it a promise "it's infinite all right".

I feel like I need some converse of lazy matching: some lazy returning where I can tell the evaluator the WHNF of this comes out as (:), but you'll still need to call this thunk later to find out what's in it.

It also still feels like a fixed point, but I haven't managed to express in a way that worked.

Ricer answered 8/1, 2019 at 17:6 Comment(7)
So, same as last time, my problem is more involved (though not so much more as last time), but this my current least blocking point. I'm not even to the point of blocking on writing the patching function, I can't even write the generator. My initializers are neither 1 nor comparable; my operation is not (+).Ricer
"the evaluator can't decide whether the generated structure is finite or not [...]" The evaluator doesn't care; it will just evaluate what you tell it to. If there's a loop, you've self-referenced somewhere.Charpoy
If you're doing more with multidimensional infinite recursive structures, you might want to step away from lists toward a trie, for better performance and less awkwardness.Ferrotype
I've edited my answer to answer your main question. (In case you missed the notification.)Tarpaulin
@AJFarmar: this is neither useful nor constructive. I know why there's a loop. I detailed it a paragraph above what you're quoting. I know the evaluator doesn't care. Or have any other feeling, FTM. I feel that entire previous section should make it clear I have a vague idea how the evaluation works; so if it wasn't obvious enough here it is spelled out: that sentence is intended neither as an answer not without context.Ricer
@Li-yaoXia: didn't miss anything, was just AFK at the wrong time. Sorry for the induced stress.Ricer
@luqui: here's my attempt (as an answer here) at using tries. Your feedback welcome!Ricer
T
4

Here's a lazier version of zipWith that makes your example productive. It assumes the second list is at least as long as the first, without forcing it.

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)

Looking at the matrix we want to define:

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

There is a simple relationship between the matrix and the remainder, that describes the fact that each entry in the remainder is obtained by summing the entry to its left and the one above it: take the sum of the matrix without its first row, and the matrix without its first column.

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

From there, we can apply a patch/padding function to the remainder, to fill in the first row and first column, and edit whatever elements. Those modifications will be fed back through the recursive occurences of matrix. This leads to the following generalized definition of 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))

For example, the simplest padding function is to complete the matrix with an initial row and column.

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

Here we have to be careful to be lazy in the remainder, since we're in the middle of defining it, hence the use of zipWith' defined above. Said another way, we must ensure that if we pass undefined to rowCol row col we can still see the initial values that the rest of the matrix can be generated from.

Now pascals can be defined as follows.

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

Helper to truncate infinite matrices:

trunc :: [[Integer]] -> [[Integer]]
trunc = map (take 10) . take 10
Tarpaulin answered 8/1, 2019 at 17:21 Comment(2)
This is so PERFECT. Thank you so much! I'll posit the following comments: 1) thank you for the word "productive", that I've been dearly missing while writing the question; 2) I like that you call "simple" the (zipWith . zipWith) expression :-) It'll take me some time, but I'm not calling intuitive yet (yet I write (f .) . (+)-like expressions all the time, fancy that); 3) Ironic to call it zipWith', considering the foldl/foldl' situation; 4) So no way to do it with the standard library?Ricer
Right I don't know of any common place where zipWith' can be imported from. You should try zipWith . zipWith . zipWith :) I think the types of those things are quite indicative here.Tarpaulin
R
2

For comparison's sake, I've written an alternate version using Data.IntTrie as suggested by @luqui.

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

Using the following Trie2D structure:

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

And support code:

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] ]

Let's not forget the patching function, it is the underlying cause of the entire question:

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

Took a bit of time, but very satisfying results. Thanks for giving me the opportunity to try this out!

I do enjoy the ease of writing once the support code is up and running.

Comments welcome! Forgive me for forcing the Bits instance to Int a lot, but the code is hairy enough as is.

Ricer answered 11/1, 2019 at 15:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.