------------------------------------------------------------------------------- A simple interactive tree editor Mark P Jones, September 19, 1996 ================================ ================================= Part 1: Forests and trees ------------------------- We use standard datatypes to represent forests of so-called general trees:> type Forest a = [Node a] -- A Forest is a list of nodes, each of> data Node a = Node a (Forest a) -- which has a value and some children. Here is a simple example:> myForest :: Forest String> myForest = [Node "1"> [Node "1.1"> [Node "1.1.1" [],Node "1.1.2"[]],> Node "1.2" []],> Node "2" []]> t1 = putStrLn(showForest myForest) Next we define two simple, general purpose operations for working with forests. First, forestElems, which enumerates the values in a forest in depth first order:> forestElems :: Forest a -> [a]> forestElems = concat . map nodeElems> where nodeElems (Node x cs) = x : forestElems cs The second function is depthMap, which traverses a forest and creates a new one of the same shape by applying a function that takes an extra parameter supplying the depth of the tree at that point:> depthMap :: (Int -> a -> b) -> Int -> Forest a -> Forest b> depthMap f d = map depthNode> where depthNode (Node x cs) = Node (f d x) (depthMap f (d+1) cs) These functions can be used to help display the structure of a forest:> showForest :: Forest String -> String> showForest = unlines . forestElems . depthMap indent 1> where indent d x = replicate (2*d) ' ' ++ x Part 2: Navigation ------------------ For the purposes of navigation, we need to have a way of describing positions within a forest. For any position, we need to capture: o The nodes to the left of the current position (which we will keep in a list with rightmost element first, that is, in reverse order). o The nodes to the right of the current position, also in a list. o A sequence of levels up the tree, from the current position to the root. We need to know the position within each level, which we represent by a triple (left, x, right) where left and right are the siblings on either side, and x is the value of the dominating node. This leads naturally to the following datatype definition:> data Position a = Pos {left::[Node a], up::[Level a], right::[Node a]}> type Level a = ([Node a], a, [Node a]) It is fairly easy to convert between forests and positions (although the position information is lost, because there can be many different positions within a given forest):> rootPosition :: Forest a -> Position a> rootPosition f = Pos [] [] f> reconstruct :: Position a -> Forest a> reconstruct (Pos ls us rs) = foldl recon (reverse ls ++ rs) us> where recon fs (ls,x,rs) = reverse ls ++ [Node x fs] ++ rs The following function finds the value (if any) associated with the node on the immediate right of current position:> rightValue :: Position a -> Maybe a> rightValue (Pos _ _ (Node x _ : _)) = Just x> rightValue _ = Nothing There are four functions for moving around in a forest, either to the left, to the right, up, or down. In the last case, there are two possibilities: down the tree on the immediate left of the current position, or down the tree on the immediate right. For simplicity, we will only consider the latter. All of these functions could fail if the requested move is not possible, so the resulting position is returned in a Maybe type.> moveUp, moveDown, moveLeft, moveRight :: Position a -> Maybe (Position a)> moveLeft (Pos ls us rs)> = repos ls (\n ns -> Pos ns us (n:rs))> moveRight (Pos ls us rs)> = repos rs (\n ns -> Pos (n:ls) us ns)> moveDown (Pos ls us rs)> = repos rs (\(Node x cs) ns -> Pos [] ((ls,x,ns):us) cs)> moveUp (Pos ls us rs)> = repos us (\(as,x,bs) vs -> Pos as vs (make x : bs))> where make x = Node x (reverse ls ++ rs) Each of these functions works by inspecting a list and taking some action if it is non-empty -- which signals that a move is possible. We capture this general pattern in the following repositioning function:> repos :: [b] -> (b -> [b] -> Position a) -> Maybe (Position a)> repos [] f = Nothing> repos (x:xs) f = Just (f x xs) We will also want simple methods for inserting and deleting nodes to the right of the current position (we won't bother with the obvious duals for insertion or deletion on the left).> insertNode :: a -> Position a -> Position a> insertNode x (Pos ls us rs)> = Pos ls us (Node x [] : rs)> deleteNode :: Position a -> Maybe (Position a)> deleteNode (Pos ls us rs)> = repos rs (\_ ns -> Pos ls us ns) As a mildly amusing little extension, we can define a reflect operator:> reflect :: Position a -> Position a> reflect (Pos ls us rs) = Pos rs us ls This could have been used to define moveLeft in terms of moveRight (or vice versa). Part 3: User interface ---------------------- The main interactive process is defined as follows:> type Pos = Position String> mip :: Pos -> IO ()> mip p = do ch <- getChar> putChar '\n'> case ch of> '\n' -> mip p -- whitespace> '\t' -> mip p> ' ' -> mip p> -- basic movement> 'f' -> tryTo p moveDown noNode mip -- F irst child > 'n' -> tryTo p moveRight noNode mip -- N ext sibling> 'b' -> tryTo p moveLeft noPrev mip -- B ack to previous sibling> 'p' -> tryTo p moveUp noPar mip -- P arent> > 'd' -> tryTo p deleteNode noNode mip -- delete and insert>> 'i' -> do key <- getLine -- use as: i