Использование системы типа Haskell для обеспечения модульности
Я думаю о том, как использовать систему типа Haskell для обеспечения модульности в программе. Например, если у меня есть веб-приложение, мне любопытно, есть ли способ отделить весь код базы данных от кода CGI от кода файловой системы от чистого кода.
Например, я представляю монаду DB, поэтому я мог писать такие функции, как:
countOfUsers :: DB Int
countOfUsers = select "count(*) from users"
Я бы хотел, чтобы было невозможно использовать побочные эффекты, отличные от тех, которые поддерживаются монадой DB. Я представляю монаду более высокого уровня, которая была бы ограничена прямыми обработчиками URL-адресов и могла бы составлять вызовы в монаду БД и монаду IO.
Возможно ли это? Это мудрое?
Обновление. Я закончил достижение этого с помощью Scala вместо Haskell: http://moreindirection.blogspot.com/2011/08/implicit-environment-pattern.html p >
Ответы
Ответ 1
Я представляю монаду более высокого уровня, которая была бы ограничена прямыми обработчиками URL-адресов и могла бы составлять вызовы на монаду БД и монаду IO.
Вы можете, конечно, добиться этого и получить очень сильные статические гарантии относительно разделения компонентов.
В самом простом, вам нужна ограниченная монашка. Используя что-то вроде техники "tainting", вы можете создать набор операций ввода-вывода, поднятых в простую оболочку, затем использовать систему модулей, чтобы скрыть базовые конструкторы для типов.
Таким образом, вы сможете запускать код CGI в контексте CGI и код DB в контексте БД. В Hackage есть много примеров.
Другой способ - построить интерпретатор для действий, а затем использовать конструкторы данных для описания каждой примитивной операции, которую вы желаете. Операции должны по-прежнему формировать монаду, и вы можете использовать do-notation, но вместо этого вы создадите структуру данных, которая описывает действия для запуска, которые затем выполняются контролируемым образом через интерпретатор.
Это дает вам, возможно, больше интроспекции, чем вам нужно в типичных случаях, но этот подход дает вам полную возможность проверять код пользователя перед его выполнением.
Ответ 2
Я думаю, что там третий путь за пределами двух дон Стюарт, который может быть даже проще:
class Monad m => MonadDB m where
someDBop1 :: String -> m ()
someDBop2 :: String -> m [String]
class Monad m => MonadCGI m where
someCGIop1 :: ...
someCGIop2 :: ...
functionWithOnlyDBEffects :: MonadDB m => Foo -> Bar -> m ()
functionWithOnlyDBEffects = ...
functionWithDBandCGIEffects :: (MonadDB m, MonadCGI m) => Baz -> Quux -> m ()
functionWithDBandCGIEffects = ...
instance MonadDB IO where
someDBop1 = ...
someDBop2 = ...
instance MonadCGI IO where
someCGIop1 = ...
someCGIop2 = ...
Идея очень проста в том, что вы определяете классы типов для различных подмножеств операций, которые вы хотите разделить, а затем параметризируйте свои функции, используя их. Даже если единственная конкретная монада, которую вы когда-либо делали для экземпляра классов, - IO, функции, параметризованные на любом MonadDB, будут разрешены только для использования операций MonadDB (и построенных из них), чтобы вы достигли желаемого результата. И в функции "можно что-либо сделать" в монаде IO вы можете легко использовать операции MonadDB и MonadCGI, поскольку IO - это экземпляр.
(Конечно, вы можете определить другие экземпляры, если захотите. Делать операции с помощью различных трансформаторов монады были бы простыми, и я думаю, что на самом деле ничего не мешает вам писать экземпляры для "обертки" и "интерпретатора", монады Дон Стюарт упоминает, тем самым объединяя подходы - хотя я не уверен, есть ли причина, по которой вы захотите.)
Ответ 3
Спасибо за этот вопрос!
Я проделал некоторую работу над веб-картой клиента/сервера, в которой использовались монады, чтобы различать разные среды экзекуции. Очевидные из них были на стороне клиента и на стороне сервера, но также позволяли вам писать код на стороне (который мог работать как на клиенте, так и на сервере, поскольку он не содержал каких-либо специальных функций), а также асинхронную клиентскую сторону, которая был использован для написания неблокирующего кода на клиенте (по существу, монады продолжения на стороне клиента). Это звучит очень похоже на вашу идею различения кода CGI и кода DB.
Вот некоторые ресурсы о моем проекте:
Я думаю, что это интересный подход, и он может дать вам интересные гарантии относительно кода. Однако есть несколько сложных вопросов. Если у вас есть серверная функция, которая принимает int
и возвращает int
, то каков должен быть тип этой функции? В моем проекте я использовал int -> int server
(но также можно использовать server (int -> int)
.
Если у вас есть пара таких функций, то это не так просто составить их. Вместо записи goo (foo (bar 1))
вам нужно написать следующий код:
do b <- bar 1
f <- foo b
return goo f
Вы можете написать одно и то же, используя некоторые комбинаторы, но я считаю, что композиция немного менее изящна.