Отказ от ответственности : Я не могу обещать, что это хороший способ сделать это, но работа над этим звучит весело .Давайте возьмем это для вращения, не так ли?
Несколько обязательных импортов
Сначала давайте отбросим некоторые типы данных.Я собираюсь заполнить некоторые детали и немного доработать, чтобы определить простую «файловую систему», с которой мы можем реально взаимодействовать.
type Path = String
type Response = Maybe String
type Contents = [String]
data Operation = Cd Path
| Ls
| MkDir Path
| Quit
deriving (Read, Show)
Далее, мы сделаем что-то немного острый ... убери все монады.Какие?Это безумие!Возможно, но иногда вся скрытая сантехника, которую обеспечивает >>=
, скрывает вещи немного слишком очень.
Для самой файловой системы мы просто сохраним текущий рабочий каталог и карту путей к их дочерним элементам.Нам также понадобится несколько функций для взаимодействия с ним.
data Filesystem = Filesystem { wd :: Path, files :: M.Map Path Contents }
deriving Show
newFS = Filesystem "/" (M.singleton "/" [])
isDirectory p fs = M.member p $ files fs
children p fs = fromMaybe [] . M.lookup p $ files fs
cd p fs = fs { wd = p }
create p fs = let newPath = wd fs ++ p ++ "/"
addPath = M.insert newPath [] . M.adjust (p:) (wd fs)
in (newPath, fs { files = addPath $ files fs })
Теперь для безмонадной версии функции step
.Нужно взять Operation
и Filesystem
и вернуть Response
и (возможно измененный) Filesystem
:
step :: Operation -> Filesystem -> (Response, Filesystem)
step (Cd d) fs = (Just "Ok\n", cd d fs)
step (MkDir d) fs = first (\d -> Just $ "Created " ++ d ++ "\n") $ create d fs
step Ls fs = let files = children (wd fs) fs
in (Just $ unlines files, fs)
step Quit fs = (Nothing, fs)
... хмм, эта сигнатура типа уже выглядит какочень похоже на смелость State
монады.Ну что ж, просто пока проигнорируйте это и начните действовать вслепую.
Теперь нам нужна функция, которая предоставит универсальный интерфейс для интерпретатора Filesystem
.В частности, мы хотим, чтобы интерфейс был, по крайней мере, несколько самодостаточным , чтобы при любом использовании интерфейса не приходилось переходить вручную, но мы хотим, чтобы интерфейс был достаточно незаметным для кода, использующего его, которыймы можем связать его с IO
монадой, какой-то другой Monad
или даже без монады вообще.
В первую очередь это говорит нам о том, что нам нужно каким-то образом чередовать внешний код с интерпретатором , вместо того чтобы контролировать какую-либо часть.Теперь Haskell - это функциональный язык , так что это означает, что использование множества функций высшего порядка - это хорошо, верно?Звучит правдоподобно для меня, поэтому вот стратегия, которую мы будем использовать: Если функция не знает, что делать дальше, мы передадим ей другую функцию, которую мы предполагаем, делает. Повторяйте, пока все не будут знать, что происходитна.Безупречный план, не так ли?
Сердце всего этого - функция step
, поэтому мы начнем с того, что просто позвоним.
interp1 :: Operation -> Filesystem -> (Response, Filesystem)
interp1 op fs = step op fs
... ну, этоНачните.Похоже.Но подождите, откуда взялся Operation
?Нам нужен внешний код, чтобы обеспечить это, но мы не можем просто попросить об этом, не перепутав все с неприятными символами, такими как IO
.Таким образом, мы получили еще одну функцию, которая сделает за нас грязную работу:
interp2 :: ((Operation -> (Response, Filesystem)) -> t) -> Filesystem -> t
interp2 inp fs = inp (\op -> step op fs)
Конечно, теперь у нас есть только одна глупость t
, которую мы даже не знаем, что это такое.Мы знаем, что в нем где-то должны быть Response
и Filesystem
, но мы ничего не можем делать с этим, поэтому мы вернем его обратно в другую функцию вместе с некоторыми инструкциямио том, как действовать ... что, конечно, потребует передачи еще большего количества функций.Вы знаете, что он функционирует до самого конца.
interp3 :: ((Operation -> (Response, Filesystem)) -> a)
-> (a -> ((Response, Filesystem) -> b) -> c)
-> (Filesystem -> b)
-> (String -> Filesystem -> b)
-> Filesystem
-> c
interp3 inp check done out fs = check (inp (\op -> step op fs)) test
where test (Nothing, fs) = done fs
test (Just s, fs) = out s fs
... ну, это довольно уродливо.Но не волнуйтесь, все идет по плану.Далее мы можем сделать пару замечаний:
- Тип
a
существует только между inp
и check
, поэтому, оглядываясь назад, мы могли бы также объединить их заранее и просто передатьСоставная функция для интерпретатора. - Когда мы называем
done
, это должно означать именно то, что написано на банке.Таким образом, тип возвращаемого значения для done
должен совпадать с типом интерпретатора, то есть b
и c
должны быть одного типа.
Теперь, если done
завершит все это, что будет out
?Как следует из названия, не слишком тонкого, он обеспечивает вывод во внешний код, но куда он идет после этого?Он должен каким-то образом возвращаться в интерпретатор, и мы могли бы заметить, что наш интерпретатор еще не рекурсивен.Путь вперед ясен - переводчик, как и Йормунганд, таким образом захватывает свой собственный хвост;возвращаться к бесконечности до тех пор, пока не закончится интерпретация (или пока Ragnarök, в зависимости от того, что наступит раньше).
interp4 :: ((Operation -> (Response, Filesystem))
-> ((Response, Filesystem) -> r) -> r)
-> (Filesystem -> r)
-> (String -> Filesystem -> (Filesystem -> r) -> r)
-> Filesystem
-> r
interp4 checkInp done out fs = checkInp (\op -> step op fs) test
where loop = interp4 checkInp done out
test (Nothing, fs) = done fs
test (Just s, fs) = out s fs loop
... о, я упоминал, что это работает сейчас?Нет, серьезно!
Вот некоторый IO
код для использования интерфейса:
ioIn f k = putStr "> " >> (k . f =<< readLn)
ioDone fs = putStrLn "Done" >> return fs
ioOut x fs k = putStr x >> k fs
ioInterp :: IO Filesystem
ioInterp = interp4 ioIn ioDone ioOut newFS
А вот код, который запускает список команд, формируя список выходных строк:
scriptIn f k (x:xs) = k (f x) xs
scriptDone fs xs = ["Done\n"]
scriptOut r fs k xs = r : k fs xs
scriptInterp :: [Operation] -> [String]
scriptInterp = interp4 scriptIn scriptDone scriptOut newFS
Примеры запускаемых здесь обоих в GHCi , если только код недостаточно щекочет ваше воображение.
Ну, вот и все.Либо это?Честно говоря, этот переводчик - это код, который может любить только мама.Есть ли что-то, что элегантно связало бы все это?Что-то, чтобы раскрыть основную структуру кода?
... хорошо, так что довольно очевидно, к чему это приведет.Общий дизайн функций, вызывающих друг друга в кругах, выглядит очень похожим на стиль продолжения, и не один раз, а дважды в сигнатуре интерпретатора можно найти характерный образец (foo -> r) -> r
, более известныйкак монада продолжения.
К сожалению, даже после всего этого продолжения причиняют боль моей голове, и я не уверен, как лучше распутать очень специальную структуру интерпретатора в вычисление, выполняемое в MonadCont
.