Learn You a Haskell for Great Good! - Miran Lipovaca [162]
If we’re focusing on the folder "root", and we then focus on the file "dijon_poupon.doc", what should the breadcrumb that we leave look like? Well, it should contain the name of its parent folder along with the items that come before and after the file on which we’re focusing. So, all we need is a Name and two lists of items. By keeping separate lists for the items that come before the item that we’re focusing on and for the items that come after it, we know exactly where to place it once we move back up. That way, we know the location of the hole.
Here’s our breadcrumb type for the filesystem:
data FSCrumb = FSCrumb Name [FSItem] [FSItem] deriving (Show)
And here’s a type synonym for our zipper:
type FSZipper = (FSItem, [FSCrumb])
Going back up in the hierarchy is very simple. We just take the latest breadcrumb and assemble a new focus from the current focus and breadcrumb, like so:
fsUp :: FSZipper -> FSZipper
fsUp (item, FSCrumb name ls rs:bs) = (Folder name (ls ++ [item] ++ rs), bs)
Because our breadcrumb knew the parent folder’s name, as well as the items that came before our focused item in the folder (that’s ls) and the items that came after (that’s rs), moving up was easy.
How about going deeper into the filesystem? If we’re in the "root" and we want to focus on "dijon_poupon.doc", the breadcrumb that we leave will include the name "root", along with the items that precede "dijon_poupon.doc" and the ones that come after it. Here’s a function that, given a name, focuses on a file or folder that’s located in the current focused folder:
import Data.List (break)
fsTo :: Name -> FSZipper -> FSZipper
fsTo name (Folder folderName items, bs) =
let (ls, item:rs) = break (nameIs name) items
in (item, FSCrumb folderName ls rs:bs)
nameIs :: Name -> FSItem -> Bool
nameIs name (Folder folderName _) = name == folderName
nameIs name (File fileName _) = name == fileName
fsTo takes a Name and a FSZipper and returns a new FSZipper that focuses on the file with the given name. That file must be in the current focused folder. This function doesn’t search all over the place—it just looks in the current folder.
First, we use break to break the list of items in a folder into those that precede the file that we’re searching for and those that come after it. break takes a predicate and a list and returns a pair of lists. The first list in the pair holds items for which the predicate returns False. Then, once the predicate returns True for an item, it places that item and the rest of the list in the second item of the pair. We made an auxiliary function called nameIs, which takes a name and a filesystem item and returns True if the names match.
Now ls is a list that contains the items that precede the item that we’re searching for, item is that very item, and rs is the list of items that come after it in its folder. Now that we have these, we just present the item that we got from break as the focus and build a breadcrumb that has all the data it needs.
Note that if the name we’re looking for isn’t in the folder, the pattern item:rs will try to match on an empty list, and we’ll get an error. And if our current focus is a file, rather than a folder, we get an error as well, and the program crashes.
So, we can move up and down our filesystem. Let’s start at the root and walk to the file "skull_man(scary).bmp":
ghci> let newFocus = (myDisk, []) -: fsTo "pics" -: fsTo "skull_man(scary).bmp"
newFocus is now a zipper that’s focused on the "skull_man(scary).bmp" file. Let’s get the first component of the zipper (the focus itself) and see if that’s really true:
ghci> fst newFocus
File "skull_man(scary).bmp" "Yikes!"
Let’s move up and focus on its neighboring file "watermelon_smash.gif":
ghci> let newFocus2 = newFocus -: fsUp -: fsTo "watermelon_smash.gif"
ghci> fst newFocus2
File "watermelon_smash.gif" "smash!!"