Есть ли разница между "MonadIO m" и "MonadBaseControl IO m"?

Функция runTCPClient из сетевого канала имеет следующую подпись:

runTCPClient :: (MonadIO m, MonadBaseControl IO m)
             => ClientSettings m -> Application m -> m ()

MonadIO m обеспечивает

liftIO :: IO a -> m a

и MonadBaseControl IO m предоставляет

liftBase :: IO a -> m a

Нет видимой разницы. Обеспечивают ли они такую ​​же функциональность? Если да, то почему дублирование в сигнатуре типа? Если нет, то какая разница?

Ответ 1

liftBase является частью MonadBase, который является обобщением MonadIO для любой базовой монады и, как вы сказали, MonadBase IO обеспечивает ту же функциональность, что и MonadIO.

Однако MonadBaseControl является немного более сложным зверем. В MonadBaseControl IO m у вас есть

liftBaseWith :: ((forall a. m a -> IO (StM m a)) -> IO a) -> m a
restoreM     :: StM m a -> m a

Проще всего понять, что такое практическое использование, глядя на примеры. Например, bracket из base имеет подпись

bracket ::  IO a -> (a -> IO b) -> (a -> IO c) -> IO c

С помощью всего лишь MonadBase IO m (или MonadIO m) вы можете поднять основной вызов bracket в m, но действия брекетинга по-прежнему должны быть в обычном старом IO.

throw и catch являются, возможно, еще лучшими примерами:

throw :: Exception e => e -> a
catch :: Exception e => IO a -> (e -> IO a) -> IO a

Вы можете легко выбросить исключение из любого MonadIO m, и вы можете поймать исключение из IO a внутри MonadIO m, но опять же, действие, выполняемое в catch, и сам обработчик исключений должен быть IO a не m a.

Теперь MonadBaseControl IO позволяет писать bracket и catch таким образом, чтобы действия параметра также имели тип m a вместо того, чтобы ограничиваться базовой монадой. Общая реализация указанных выше функций (как и многих других) может быть найдена в пакете lifted-base. Например:

catch   :: (MonadBaseControl IO m, Exception e) => m a -> (e -> m a) -> m a
bracket :: MonadBaseControl IO m => m a -> (a -> m b) -> (a -> m c) -> m c

EDIT: И теперь, когда я действительно перечитаю ваш вопрос правильно...

Нет, я не вижу причин, почему для подписи требуются как MonadIO m, так и MonadBaseControl IO m, так как MonadBaseControl IO m должен подразумевать MonadBase IO m, что позволяет использовать ту же функциональность. Так что, может быть, это просто осталось от старой версии.

Глядя на источник, возможно, только потому, что runTCPClient вызывает sourceSocket и sinkSocket внутренне, а те требуют MonadIO. Я предполагаю, что причина, по которой все функции в пакете не просто используют MonadBase IO, состоит в том, что MonadIO более знакома людям, и большинство монад-трансформаторов имеют экземпляр, определенный для MonadIO m => MonadIO (SomeT m), но пользователям, возможно, придется писать их собственный экземпляр для MonadBase IO.