Использование системы типов Haskell для обеспечения модульности - PullRequest
15 голосов
/ 05 марта 2010

Я думаю о способах использования системы типов Haskell для обеспечения модульности в программе. Например, если у меня есть веб-приложение, мне интересно, есть ли способ отделить весь код базы данных от кода CGI от кода файловой системы от чистого кода.

Например, я представляю монаду БД, чтобы я мог написать такие функции, как:

countOfUsers :: DB Int
countOfUsers = select "count(*) from users"

Я бы хотел, чтобы было невозможно использовать побочные эффекты, кроме тех, которые поддерживаются монадой БД. Я представляю монаду более высокого уровня, которая будет ограничена прямыми обработчиками URL-адресов и сможет составлять вызовы для монады БД и монады ввода-вывода.

Возможно ли это? Это мудро?

Обновление : я добился этого с помощью Scala вместо Haskell: http://moreindirection.blogspot.com/2011/08/implicit-environment-pattern.html

Ответы [ 3 ]

13 голосов
/ 05 марта 2010

Я представляю высокоуровневую монаду, которая будет ограничена прямыми обработчиками URL и сможет составлять вызовы для монады БД и монады ввода-вывода.

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

В самом простом случае вам нужна монада с ограниченным вводом-выводом. Используя что-то вроде техники «порчи», вы можете создать набор операций ввода-вывода, поднятых в простую оболочку, а затем использовать систему модулей, чтобы скрыть базовые конструкторы для типов.

Таким образом, вы сможете запускать код CGI только в контексте CGI, а код БД - в контексте БД. Есть много примеров на Hackage.

Другой способ - создать интерпретатор для действий, а затем использовать конструкторы данных для описания каждой примитивной операции, которую вы хотите. Операции по-прежнему должны образовывать монаду, и вы можете использовать do-notation, но вместо этого вы будете создавать структуру данных, которая описывает выполняемые действия, которые затем выполняются контролируемым образом с помощью интерпретатора.

Это дает вам, возможно, больше самоанализа, чем вам требуется в типичных случаях, но этот подход дает вам полную возможность проверять код пользователя перед его выполнением.

5 голосов
/ 05 декабря 2011

Я думаю, что есть третий путь помимо двух упомянутых Доном Стюартами, который может быть даже проще:

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 (и те, которые построены из них), так что вы достигнете желаемого результата. А в функции «можно сделать все что угодно» в монаде ввода-вывода можно беспрепятственно использовать операции MonadDB и MonadCGI, поскольку IO является экземпляром.

(Конечно, вы можете определять другие экземпляры, если хотите. Одни поднять операции через различные монадные преобразователи были бы простыми, и я думаю, что на самом деле ничто не мешает вам писать экземпляры для " Дон Стюарт упоминает обертки "и" переводчик ", тем самым объединяя подходы - хотя я не уверен, есть ли причина, по которой вы захотите.)

4 голосов
/ 05 марта 2010

Спасибо за этот вопрос!

Я проделал некоторую работу над клиент-серверным веб-фреймворком, который использовал монады, чтобы различать разные среды исполнения. Очевидными были на стороне клиента и на стороне сервера , но это также позволило вам написать код для обеих сторон (который может выполняться как на клиенте, так и на сервере) потому что он не содержал никаких специальных функций), а также асинхронная клиентская сторона , которая использовалась для написания неблокирующего кода на клиенте (по существу, монада продолжения на клиентской стороне). Это звучит довольно тесно связано с вашей идеей различения кода CGI и кода БД.

Вот несколько ресурсов о моем проекте:

Я думаю, что это интересный подход, и он может дать вам интересные гарантии о коде. Есть несколько каверзных вопросов. Если у вас есть серверная функция, которая принимает int и возвращает int, то каков должен быть тип этой функции? В моем проекте я использовал int -> int server (но также возможно использовать server (int -> int).

Если у вас есть пара таких функций, то не так просто их составить. Вместо того, чтобы писать goo (foo (bar 1)), вам нужно написать следующий код:

do b <- bar 1
   f <- foo b
   return goo f

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

...