Two-dimensional zipper

前端 未结 3 1452
独厮守ぢ
独厮守ぢ 2020-12-09 16:01

Inspired by the recent question about 2d grids in Haskell, I\'m wondering if it would be possible to create a two-dimensional zipper to keep track of a position in a list of

3条回答
  •  轻奢々
    轻奢々 (楼主)
    2020-12-09 16:30

    I was looking for something similar: a way to cheaply and easily navigate (which includes going “backwards”) a doubly-infinite list of lists. Here's my take at it.

    If I read the others answers carefully, what I'm presenting here isn't really a zipper: while navigation is amortized O(1), the memory used by the zipper structure network is never released. On the other hand, it ought to tie the knot enough for “cells” to be shared no matter the path we take to get to them, which is the kind of topology we'd want on a 2D list of lists.

    To compensate, the list of lists used to generate it ought to eventually go unreferenced and garbage-collected.

    data FakeZip2D a = Z2 { z2Val   :: a
                          , goUp    :: !( Maybe (FakeZip2D a) )
                          , goDown  ::    Maybe (FakeZip2D a)
                          , goLeft  :: !( Maybe (FakeZip2D a) )
                          , goRight ::    Maybe (FakeZip2D a)
                          }
    
    fromList2 :: [[a]] -> Maybe (FakeZip2D a)
    fromList2 xss = head (head zss) where
      extended =       [ repeat Nothing ] ++
        map (\z -> [Nothing] ++ z ++ repeat Nothing) zss ++
                       [ repeat Nothing ]
      zss = zipWith4' row xss extended (drop 1 extended) (drop 2 extended)
      row xs prev cur next = Just <$> zipWith5' Z2 xs (tail prev) (tail next)
                                                      cur         (drop 2 cur)
    
      -- totally inspired by https://stackoverflow.com/a/54096748/12274
      zipWith4' f (a:as) (b:bs) ~(c:cs) ~(d:ds) =
        f a b c d : zipWith4' f as bs cs ds
      zipWith5' f (a:as) (b:bs) ~(c:cs) (d:ds) ~(e:es) =
        f a b c d e : zipWith5' f as bs cs ds es
    

    The data structure ought to be self-explanatory. Up and left can afford to be strict because we're building from singly-linked lists. AFAIK, there's no point in maing them lazy in Haskell, as they wouldn't let anything go out of scope anyway.

    The lattice is built recursively, expanding the borders of the provided input with Nothing. The lazy-enough variants of zipWith I needed are inspired from answers to another series of questions of mine on the topic.

    Here it is in action:

    demo :: IO ()
    demo = do
      let multList2 = [[ i*j | j <- [0..] ] | i <- [0..] ]
          multZ2 = fromList2 multList2
    
      let rows = iterate (>>= goDown) multZ2
          cols = map (iterate (>>= goRight)) rows
    
      putStrLn "Multiplication table"
      mapM_ (print . map z2Val) $ take 5 $ map (catMaybes . take 5) cols
    
      putStrLn "List of squares"
      let goDiag = goRight >=> goDown
      print $ map z2Val $ take 25 $ catMaybes $ iterate (>>= goDiag) multZ2
    
      putStrLn "Convoluted list of squares"
      let goDiag' = goDown >=> goRight >=> goUp >=> goLeft >=> goDiag
      print $ map z2Val $ take 25 $ catMaybes $ iterate (>>= goDiag') multZ2
    

    The interface can likely be made even easier to use by dropping the Maybes. At your own risk, naturally.

    This might be slightly off-topic as it's not a real zipper either, but it solved my problem; and since this is the question that came up when I first looked for a solution, I'm posting it here with the intent it helps someone else.

提交回复
热议问题