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