Миран Липовача - Изучай Haskell во имя добра!
Давайте создадим застёжку для списков. Чтобы изменить фокус на подсписках списка, мы перемещаемся или вперёд, или назад (тогда как при использовании деревьев мы перемещались вверх, влево или вправо). Помещённой в фокус частью будет подсписок, а кроме того, мы будем оставлять «хлебные крошки» по мере нашего движения вперёд.
А из чего состояла бы отдельная «хлебная крошка» для списка? Когда мы имели дело с бинарными деревьями, нужно было, чтобы «хлебная крошка» хранила элемент, содержащийся в корне родительского узла, вместе со всеми поддеревьями, которые мы не выбрали. Она также должна была запоминать, куда мы пошли, – влево или вправо. Поэтому требовалось, чтобы в ней содержалась вся имеющаяся в узле информация, за исключением поддерева, на которое мы решили навести фокус.
Списки проще, чем деревья. Нам не нужно запоминать, по шли ли мы влево или вправо, потому что вглубь списка можно пойти лишь одним способом. Поскольку для каждого узла существует только одно поддерево, нам также не нужно запоминать пути, по которым мы не пошли. Кажется, всё, что мы должны запоминать, – это предыдущий элемент. Если у нас есть список вроде [3,4,5] и мы знаем, что предыдущим элементом было значение 2, мы можем пойти назад, просто поместив этот элемент в «голову» нашего списка, получая [2,3,4,5].
Поскольку отдельная «хлебная крошка» здесь – просто элемент, нам на самом деле не нужно помещать её в тип данных, как мы делали это, когда создавали тип данных Crumb, использовавшийся застёжками для деревьев.
type ListZipper a = ([a], [a])
Первый список представляет список, на котором мы фокусируемся, а второй – это список «хлебных крошек». Давайте создадим функции, которые перемещаются вперёд и назад по спискам:
goForward :: ListZipper a –> ListZipper a
goForward (x:xs, bs) = (xs, x:bs)
goBack :: ListZipper a –> ListZipper a
goBack (xs, b:bs) = (b:xs, bs)
Когда мы движемся вперёд, мы фокусируемся на «хвосте» текущего списка и оставляем головной элемент в качестве «хлебной крошки». При движении назад мы берём самую последнюю «хлебную крошку» и помещаем её в начало списка. Вот эти две функции в действии:
ghci> let xs = [1,2,3,4]
ghci> goForward (xs, [])
([2,3,4], [1])
ghci> goForward ([2,3,4], [1])
([3,4], [2,1])
ghci> goForward ([3,4], [2,1])
([4], [3,2,1])
ghci> goBack ([4], [3,2,1])
([3,4], [2,1])
Вы можете видеть, что «хлебные крошки» в случае со списками – просто перевёрнутая часть вашего списка. Элемент, от которого мы удаляемся, всегда помещается в «голову» «хлебных крошек». Потом легко переместиться назад, просто вынимая этот элемент из их «головы» и делая его «головой» нашего фокуса. На данном примере опять-таки легко понять, почему мы называем это застёжкой: действительно очень напоминает перемещающийся вверх-вниз замок застёжки-молнии!
Если бы вы создавали текстовый редактор, можно было бы использовать список строк для представления строк текста, которые в текущий момент открыты, а затем использовать застёжку, чтобы знать, на какой строке в данный момент установлен курсор. Использование застёжки также облегчило бы вставку новых строк в любом месте текста или удаление имеющихся строк.
Очень простая файловая система
Для демонстрации работы застёжек давайте используем деревья, чтобы представить очень простую файловую систему. Затем мы можем создать застёжку для этой файловой системы, которая позволит нам перемещаться между каталогами, как мы это делаем при переходах по реальной файловой системе.
Обычная иерархическая файловая система состоит преимущественно из файлов и каталогов. Файлы – это элементы данных, снабжённые именами. Каталоги используются для организации этих файлов и могут содержать файлы или другие каталоги. Для нашего простого примера достаточно сказать, что элементами файловой системы являются:
• файл под некоторым именем, содержащий некие данные;
• каталог под некоторым именем, содержащий другие элементы, которые сами являются или файлами, или каталогами.
Вот соответствующий тип данных и некоторые синонимы типов, чтобы было понятно, что к чему:
type Name = String
type Data = String
data FSItem = File Name Data | Folder Name [FSItem] deriving (Show)
К файлу прилагаются две строки, представляющие его имя и содержимое. К каталогу прилагаются строка, являющаяся его именем, и список элементов. Если этот список пуст, значит, мы имеем пустой каталог.
Вот каталог с некоторыми файлами и подкаталогами (на самом деле это то, что в настоящую минуту содержится на моём диске):
myDisk :: FSItem
myDisk =
Folder "root"
[ File "goat_yelling_like_man.wmv" "бааааааа"
, File "pope_time.avi" "Боже, благослови"
, Folder "pics"
[ File "ape_throwing_up.jpg" "блин..."
, File "watermelon_smash.gif" "шмяк!!"
, File "skull_man(scary).bmp" "Ой!"
]
, File "dijon_poupon.doc" "лучшая горчица"
, Folder "programs"
[ File "sleepwizard.exe" "10 пойти поспать"
, File "owl_bandit.dmg" "move ax, 42h"
, File "not_a_virus.exe" "точно не вирус"
, Folder "source code"
[ File "best_hs_prog.hs" "main = print (fix error)"
, File "random.hs" "main = print 4"
]
]
]
Создаём застёжку для нашей файловой системы
Теперь, когда у нас есть файловая система, всё, что нам нужно, – это застёжка, чтобы мы могли застёгивать файловую систему и брать её крупным планом, а также добавлять, изменять и удалять файлы и каталоги. Как и в случае с использованием бинарных деревьев и списков, наши «хлебные крошки» будут содержать информацию обо всём, что мы решили не посещать. Отдельная «хлебная крошка» должна хранить всё, кроме поддерева, на котором мы фокусируемся в данный момент. Она также должна указывать, где находится отверстие, чтобы при перемещении обратно вверх мы смогли вставить в отверстие наш предыдущий фокус.
В этом случае «хлебная крошка» должна быть похожа на каталог – только выбранный нами в данный момент каталог должен в нём отсутствовать. Вы спросите: «А почему не на файл?» Ну, потому что, когда мы фокусируемся на файле, мы не можем углубляться в файловую систему, а значит, не имеет смысла оставлять «хлебную крошку», которая говорит, что мы пришли из файла. Файл – это что-то вроде пустого дерева.
Если мы фокусируемся на каталоге "root", а затем на файле "dijon_poupon.doc", как должна выглядеть «хлебная крошка», которую мы оставляем? Она должна содержать имя своего родительского каталога вместе с элементами, идущими перед файлом, на котором мы фокусируемся, и следом за ним. Поэтому всё, что нам требуется, – значение Name и два списка элементов. Храня два отдельных списка для элементов, идущих перед элементом, на котором мы фокусируемся, и для элементов, идущих за ним, мы будем точно знать, где мы его поместили, при перемещении обратно вверх. Таким образом, нам известно местоположение отверстия.
Вот наш тип «хлебной крошки» для файловой системы:
data FSCrumb = FSCrumb Name [FSItem] [FSItem]
deriving (Show)
А вот синоним типа для нашей застёжки:
type FSZipper = (FSItem, [FSCrumb])
Идти обратно вверх по иерархии очень просто. Мы берём самую последнюю «хлебную крошку» и собираем новый фокус из текущего фокуса и «хлебной крошки» следующим образом:
fsUp :: FSZipper –> FSZipper
fsUp (item, FSCrumb name ls rs:bs) = (Folder name (ls ++ [item] ++ rs), bs)
Поскольку нашей «хлебной крошке» были известны имя родительского каталога, а также элементы, которые шли перед находящимся в фокусе элементом каталога (то есть ls), и элементы, которые шли за ним (то есть rs), перемещаться вверх было легко.
Как насчёт продвижения вглубь файловой системы? Если мы находимся в "root" и хотим сфокусироваться на файле "dijon_poupon. doc", оставляемая нами «хлебная крошка» будет включать имя "root" вместе с элементами, предшествующими файлу "dijon_poupon.doc", и элементами, идущими за ним. Вот функция, которая, получив имя, фокусируется на файле или каталоге, расположенном в текущем каталоге, куда в текущий момент наведён фокус:
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 принимает значения Name и FSZipper и возвращает новое значение FSZipper, которое фокусируется на файле с заданным именем. Этот файл должен присутствовать в текущем каталоге, находящемся в фокусе. Данная функция не производит поиск везде – она просто смотрит в текущем каталоге.