Объединить состояние с действиями IO
Предположим, что у меня есть монада состояний, такая как:
data Registers = Reg {...}
data ST = ST {registers :: Registers,
memory :: Array Int Int}
newtype Op a = Op {runOp :: ST -> (ST, a)}
instance Monad Op where
return a = Op $ \st -> (st, a)
(>>=) stf f = Op $ \st -> let (st1, a1) = runOp stf st
(st2, a2) = runOp (f a1) st1
in (st2, a2)
с функциями типа
getState :: (ST -> a) -> Op a
getState g = Op (\st -> (st, g st)
updState :: (ST -> ST) -> Op ()
updState g = Op (\st -> (g st, ()))
и т.д. Я хочу объединить различные операции в этой монаде с действиями IO. Поэтому я мог бы написать цикл оценки, в котором выполнялись операции в этой монаде, и с результатом выполнялось действие IO или, я думаю, я мог бы сделать что-то вроде следующего:
newtype Op a = Op {runOp :: ST -> IO (ST, a)}
Функции печати будут иметь тип Op(), а другие функции будут иметь тип Op a, например, я мог бы прочитать символ из терминала, используя функцию типа IO Char. Однако я не знаю, как выглядит такая функция, поскольку, например, недопустимо следующее.
runOp (do x <- getLine; setMem 10 ... (read x :: Int) ... ) st
поскольку getLine имеет тип IO Char, но это выражение будет иметь тип Op Char. В общих чертах, как бы я это сделал?
Ответы
Ответ 1
Основной подход - переписать вашу монаду Op
как монадный трансформатор. Это позволит вам использовать его в "стеке" монадов, нижняя часть которых может быть IO
.
Вот пример того, как это может выглядеть:
import Data.Array
import Control.Monad.Trans
data Registers = Reg { foo :: Int }
data ST = ST {registers :: Registers,
memory :: Array Int Int}
newtype Op m a = Op {runOp :: ST -> m (ST, a)}
instance Monad m => Monad (Op m) where
return a = Op $ \st -> return (st, a)
(>>=) stf f = Op $ \st -> do (st1, a1) <- runOp stf st
(st2, a2) <- runOp (f a1) st1
return (st2, a2)
instance MonadTrans Op where
lift m = Op $ \st -> do a <- m
return (st, a)
getState :: Monad m => (ST -> a) -> Op m a
getState g = Op $ \st -> return (st, g st)
updState :: Monad m => (ST -> ST) -> Op m ()
updState g = Op $ \st -> return (g st, ())
testOpIO :: Op IO String
testOpIO = do x <- lift getLine
return x
test = runOp testOpIO
Ключевое значение для наблюдения:
- Использование класса
MonadTrans
- Использование функции
lift
, действующей на getLine
, которая используется для вывода функции getLine
из монады IO
и в монаду Op IO
.
Кстати, если вы не хотите, чтобы монада IO
всегда присутствовала, вы можете заменить ее на монаду Identity
в Control.Monad.Identity
. Монада Op Identity
ведет себя точно так же, как ваша оригинальная монада Op
.
Ответ 2
Использовать liftIO
Ты уже очень близко! Ваше предложение
newtype Op a = Op {runOp :: ST -> IO (ST, a)}
отличный и способ пойти.
Чтобы выполнить getLine
в контексте Op
, вам нужно "поднять" операцию IO
в монаду Op
. Вы можете сделать это, написав функцию liftIO
:
liftIO :: IO a -> Op a
liftIO io = Op $ \st -> do
x <- io
return (st, x)
Теперь вы можете написать:
runOp (do x <- liftIO getLine; ...
Использовать класс MonadIO
Теперь шаблон подъема действия ввода-вывода в пользовательскую монаду настолько распространен, что для него есть класс стандартного типа:
import Control.Monad.Trans
class Monad m => MonadIO m where
liftIO :: IO a -> m a
Итак, ваша версия liftIO
будет вместо экземпляра MonadIO
:
instance MonadIO Op where
liftIO = ...
Использовать StateT
В настоящее время вы создали собственную версию государственной монады, специализирующейся на состоянии ST
. Почему бы вам не использовать стандартную государственную монаду? Это избавит вас от необходимости писать свой собственный экземпляр Monad
, который всегда остается неизменным для государственной монады.
type Op = StateT ST IO
StateT
уже имеет экземпляр Monad
и экземпляр MonadIO
, поэтому вы можете использовать их немедленно.
Трансформаторы Monad
StateT
является так называемым монадным трансформатором. Вам нужны только действия IO
в вашей монаде Op
, поэтому я уже специализировал ее с монадой IO
для вас (см. Определение type Op
). Но монадные трансформаторы позволяют вам складывать произвольные монады. Об этом говорит intoverflow. Вы можете прочитать о них здесь и здесь.