Использование системы типа 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

Вы можете написать одно и то же, используя некоторые комбинаторы, но я считаю, что композиция немного менее изящна.