Объединить состояние с действиями ввода-вывода - PullRequest
27 голосов
/ 04 сентября 2010

Предположим, у меня есть монада состояний, такая как:

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, ()))

и т. Д. Я хочу объединить различные операции в этой монаде с действиями ввода-вывода. Таким образом, я мог бы написать цикл оценки, в котором выполнялись операции в этой монаде и выполнялось действие ввода-вывода с результатом, или, я думаю, я мог бы сделать что-то вроде следующего:

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. В общих чертах, как бы я это сделал?

Ответы [ 2 ]

31 голосов
/ 04 сентября 2010

Используйте 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 = ...

Use StateT

Вы в настоящее время написали свою собственную версию монады состояния, специализированную для состояния ST.Почему вы не используете стандартную государственную монаду?Это избавляет вас от необходимости писать собственный экземпляр Monad, который всегда одинаков для монады состояния.

type Op = StateT ST IO

StateT уже имеет экземпляр Monad и экземпляр MonadIO,поэтому вы можете использовать их немедленно.

Монадные трансформаторы

StateT - это так называемый монадный трансформатор .Вам нужно только IO действий в вашей монаде Op, поэтому я уже специализировал ее на монаде IO для вас (см. Определение type Op).Но преобразователи монад позволяют вам составлять произвольные монады.Это то, о чем говорит inverflow.Вы можете прочитать больше о них здесь и здесь .

25 голосов
/ 04 сентября 2010

Основной подход заключается в том, чтобы переписать вашу 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.

...