-- Zipper over the Map with path accumulation -- and sharing preservation -- $Id: ZipperM.hs,v 1.4 2005/09/22 03:06:38 oleg Exp $ module ZipperM (Term(..) , FileName , FileCont , NavigateDir(..) , DZipper(..) , dzip'term , module CCCxe ) where -- Import (one of the) CC libraries -- http://okmij.org/ftp/continuations/implementations.html#CC-monads -- We chose CCCxe; CCExe would work just as well -- We chose the prompt flavor PP import CCCxe import Control.Monad.Identity import Control.Monad.Trans import Data.Map as Map import Data.Typeable -- ------------------------------------------------------------- -- The Term to traverse: the representation of a file system -- Currently, the file system is in-memory; in virtual memory, to be precise -- It is easy to change the Term below so that "File" only points to file -- data rather than containing them. type FileName = String type FileCont = String -- File content data Term = File FileCont | Folder (Map.Map FileName Term) instance Show Term where showsPrec _ (File file) = (file ++) showsPrec _ (Folder dir) = ("\n >>>" ++) . (Map.foldWithKey fl ("\n<<<" ++) dir) where fl k term acc = ("\n" ++) . (k ++) . (": " ++) . (showsPrec 5 term) . acc -- ------------------------------------------------------------- -- Traversable of Term -- We now derive the function to traverse and update a Term. -- The traverse function is a variant of fmap, or of Traversable.mapM -- to be precise. -- The fmap function assumes that every visited node will be updated, -- by the mapping function, the iteratee. -- Our traverse is optimized for the opposite case: most of the -- traversed nodes will not be updated. If a branch is not modified, -- it should not be copied into the result; rather, it should be -- shared with the original term. Furthermore, we permit the -- iteratee to bypass the branches of the term during the traversal. -- Index for a place (node) in the file system tree -- It is the list of FileNames to navigate from the root of the -- tree, in the REVERSE order data PathComp = PathName FileName | PathUpdated -- The update indicator deriving (Eq, Show) type Path = [PathComp] -- Where to go next: the iteratee tells the global enumerator where -- to go next. The iteratee may also tell to update the current -- term data NavigateDir = Update Term -- replace the current term with the given | DownTo FileName | Up | Next deriving (Show) -- If the term has been updated during the traversal data UpdateStatus = Dirty | Unmodified deriving Show -- Updateable traversal that maximally preserves the sharing -- If the Iteratee always returns Next, the whole tree is traversed -- pre-order (to be precise, the directory is given to the iteratee -- before and after traversing any child node) -- The Iteratee may ask to skip the branches, returning -- Up -- return to the parent; skipping the rest of the current folder -- DownTo -- go immediately to the specified child in the folder -- skipping what was before in the directory order -- If the Iteratee returns (Update term), the current term -- is replaced with the given, and the traversal restarts from it traverse :: Monad m => (Path -> Term -> m NavigateDir) -- traversal fn, iteratee -> Term -- term to traverse -> m (Term, UpdateStatus) traverse tf term = loop Nothing [] term where loop comefrom path term = tf path term >>= \x -> case x of Update new_term -> loop comefrom (PathUpdated:path) new_term -- re-traverse Up -> up path term -- cut nav -> down comefrom path term nav up (PathUpdated:_) term = return (term,Dirty) up _ term = return (term,Unmodified) down _ path term@File{} Next = up path term down _ _ File{} nav = error $ "bad navigation from a File: " ++ show nav down _ path pt@(Folder fld) (DownTo fname) | Just term <- Map.lookup fname fld = child fname path fld term down _ path _ (DownTo fname) = error $ "No folder component: " ++ fname down _ path pt@(Folder fld) Next | Map.null fld = up path pt down Nothing path pt@(Folder fld) Next | (fname, term) <- Map.findMin fld = child fname path fld term down (Just fname) path pt@(Folder fld) Next | idx <- Map.findIndex fname fld = let idx' = succ idx in if idx' == Map.size fld then up path pt else let (fname,term) = Map.elemAt idx' fld in child fname path fld term child fname path fld term = loop Nothing (PathName fname:path) term >>= \x -> case x of (term, Dirty) -> loop (Just fname) (PathUpdated:path) (Folder (Map.insert fname term fld)) _ -> loop (Just fname) path (Folder fld) -- A sample file system, to be used in examples fs1 :: Term fs1 = Folder $ Map.fromList [("d1",d1), ("d2",Folder $ Map.empty), ("fl1", File "File1"), ("fl2", File "File2")] where d1 = Folder $ Map.fromList [("fl13",File "File 3"), ("d11", d11)] d11 = Folder $ Map.fromList [("d111", Folder $ Map.empty)] testt1 = runIdentity (traverse (\_ term -> return Next) fs1) -- *Zipper2> fst testt1 == fs1 -- True testt2 = traverse tf fs1 where tf path term = do print path; print term; return Next testt3 = traverse tf fs1 where tf (PathName "d11":_) term = do print "cutting" print term return Up tf path term = do print term return Next testt4 = runIdentity (traverse tf fs1) where tf (PathName "d11":_) _ = return (Update $ Folder $ Map.empty) tf (PathName "fl2":_) _ = return (Update $ File $ "New file2") tf _ _ = return Next lprint x = liftIO $ print x -- ------------------------------------------------------------- -- Zipper data structure -- It is generic: -- It depends only on the _interface_ of the traversal function -- (but not on its implementation) -- One may say, why not to put path accumulation into `traverse' itself? -- We could have. However, we wish to illustrate here that the traverse -- deals only with the local information. Accumulating it into a global -- state is left for the clients. Zipper can let us add a new, `missing' -- aspect to the enumerator. data DZipper m = DZipper{ dz_path :: [FileName], dz_term :: Term, dz_k :: NavigateDir -> CC PP m (DZipper m) } | DZipDone Term instance Typeable1 m => Typeable (DZipper m) where typeOf x = mkTyConApp (mkTyCon "ZipperM.DZipper") [typeOf1 $ mof x] where mof :: DZipper m -> m (); mof = undefined -- One prompt, used by the generator (the yield/enumerate pair) -- We instantiate the global pp to the desired answer-type. pz :: Typeable1 m => Prompt PP m (DZipper m) pz = pp dzip'term :: (Typeable1 m, Monad m) => Term -> CC PP m (DZipper m) dzip'term term = pushPrompt pz (traverse tf term >>= return . DZipDone . fst) where tf path term = shift0P pz (\k -> return $ DZipper (dir_path path) term k) dir_path = foldr filterfn [] filterfn (PathName fname) acc = fname : acc filterfn _ acc = acc testdz1 :: IO () testdz1 = runCC $ do dz <- dzip'term fs1 let loop (DZipDone term) = lprint "Finished" >> lprint term loop dz = do lprint $ (show $ dz_path dz) lprint $ dz_term dz dz_k dz Next >>= loop loop dz