Как скрыть контекст и монаду `IO` в другую монаду? - PullRequest
3 голосов
/ 16 апреля 2019

Я пытаюсь реализовать небольшое настольное приложение, используя HDBC и Haskell.GI. Я строю свои окна и диалоги, используя glade, и загружаю их с GtkBuilder. После реализации нескольких сценариев я в конечном итоге использовал один и тот же шаблон, составляя «действия» в do блоках с сигнатурой:

Connection -> Builder -> a -> IO b

Эти "действия" сочиняются в контексте монады IO, главная проблема в том, что мне приходится передавать свои Connection и Builder вокруг. Еще одна проблема, которую я предвижу, заключается в том, что если я захочу добавить еще одну внешнюю зависимость к своему приложению (например, доступ к сканеру изображений), мне придется изменить сигнатуру всех моих «действий» и, что более важно, их арность.

Что я мог сделать: я мог определить синоним типа:

type Action a b = Connection -> Builder -> a -> IO b

Я также мог бы создать именованный кортеж для устранения проблемы арности:

data Context =
    Context {
        conn :: Connection,
        builder :: Builder}

Но, тем не менее, это не решает тот факт, что каждый раз, когда я хочу получить доступ к базе данных, мне придется вызывать (conn ctx) или использовать привязку let в каждом действии.

То, что я чувствую, было бы идеально, если бы я создал свою собственную монаду, в которой я мог бы составлять свои действия, и я бы не говорил явно о своих Connection или Builder значениях.

Как бы мне определить такое монаду, зная, что IO уже является монадой?

Кстати, это как-то связано с State монадой?

1 Ответ

1 голос
/ 17 апреля 2019

[..] главная проблема в том, что я должен передать свои Connection и Builder вокруг.

Так что это часть "среды", в которой вычитать из (неоднократно).Для этого и нужна монада Reader.Пакет mtl содержит преобразователь монад ReaderT, который добавляет функциональность считывателя к базовой монаде, в вашем случае IO.

Демонстрация:

Предполагается, чтопростое действие, например ..

no_action :: Connection -> Builder -> Int -> IO Int
no_action _ _ i = return (i + 1)

Вы можете поместить это в новую монаду, которая похожа на IO, но с доступом как к соединению, так и к компоновщику, определив Context и применив преобразователь монады:

data Context = Context { connection :: Connection
                       , builder :: Builder }
type CBIO b = ReaderT Context IO b

Подъем ваших действий в эту новую (комбинированную) монаду заслуживает отдельной функции:

liftCBIO :: (Connection -> Builder -> a -> IO b) -> (a -> CBIO b)
liftCBIO f v = do
    context <- ask
    liftIO (f (connection context) (builder context) v)

Тогда вы всегда можете написать (liftCBIO no_action) num или ...

cbio_no_action = liftCBIO no_action

... и cbio_no_action num.

Для запуска вашей новой монады вы бы использовали runReaderT .. но это также заслуживает лучшего названия:

runWithInIO = flip runReaderT

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

Использование вышеизложенного выглядит следующим образом:

main = do
    i <- runWithInIO (Context Connection Builder) $ do
        a <- cbio_no_action 20
        liftIO $ putStrLn "Halfway through!"
        b <- cbio_no_action 30
        return (a + b)
    putStrLn $ show i

(Полная демонстрация на ideone)

...