Как мне на самом деле выполнить монаду StateT вместе с IO?

Я пытаюсь следовать совету, данному в разделе Объединить состояние с действиями ввода-вывода для создания AppState вместе с монадой IO. Что я получил, так это:

module Main where

import Control.Monad.State
import Control.Monad.Trans

data ST = ST [Integer] deriving (Show)
type AppState = StateT ST IO

new = ST []

append :: Integer -> State ST ()
append v = state $ \(ST lst) -> ((), ST (lst ++ [v]))

sumST :: State ST Integer
sumST = state $ \(ST lst) -> (sum lst, ST lst)

script = do
    append 5
    append 10
    append 15
    sumST

myMain :: AppState ()
myMain = do
    liftIO $ putStrLn "myMain start"
    let (res, st) = runState script new
    liftIO $ putStrLn $ show res
    liftIO $ putStrLn "myMain stop"

main = runStateT myMain (ST [15])

Есть часть этого, которую я не понимаю. Меня очень беспокоит, что у меня есть script и myMain и main. Меня также беспокоит, что я должен выполнить runState внутри myMain и что я должен передать начальное состояние в runStateT в моей основной функции. Я хочу, чтобы мой «скрипт», так сказать, находился непосредственно в функции myMain, потому что весь смысл myMain заключается в том, чтобы иметь возможность запускать добавление и суммирование непосредственно в myMain и прямо рядом с операциями печати. Я думаю, что я должен быть в состоянии сделать это вместо этого:

myMain :: AppState ()
myMain = do
    liftIO $ putStrLn "myMain start"
    append 5
    append 10
    append 15
    r <- sumST
    liftIO $ putStrLn $ show res
    liftIO $ putStrLn "myMain stop"

main = runState myMain

Я думал, что цель монадного преобразователя заключается в том, чтобы я мог выполнять свои операции с монадой State в функции (как указано выше) и поднимать операции ввода-вывода в эту функцию. Как правильно настроить все это, чтобы я мог удалить один из уровней косвенности?


В дополнение к решению Даниэля (которое я пометил как решение), я также нашел несколько вариантов, которые также могут пролить свет на ситуацию. Во-первых, окончательная реализация myMain и main:

myMain :: AppState ()
myMain = do
    liftIO $ putStrLn "myMain start"
    append 5
    append 10
    append 15
    res <- sumST
    liftIO $ putStrLn $ show res
    liftIO $ putStrLn "myMain stop"

main = runStateT myMain new

Теперь различные реализации append и sumST, помимо Дэниела:

append :: Integer -> AppState ()
append v = state $ \(ST lst) -> ((), ST (lst ++ [v]))

sumST :: AppState Integer
sumST = state $ \(ST lst) -> (sum lst, ST lst)

и (обратите внимание, что изменяется только объявление типа; на самом деле вы можете полностью опустить объявление типа!)

append :: MonadState ST m => Integer -> m ()
append v = state $ \(ST lst) -> ((), ST (lst ++ [v]))

sumST :: MonadState ST m => m Integer
sumST = state $ \(ST lst) -> (sum lst, ST lst)

Мне пришло в голову, что монада AppState/StateT не совпадает с базовой монадой State, и я кодировал как sumST, так и append для монады State. В некотором смысле их также нужно было поднимать в монаду StateT, хотя правильный способ думать о in состоит в том, что они должны были запускаться в монаде (отсюда runState script new).

Я не уверен, что полностью понял это, но я поработаю с ним некоторое время, прочитаю код MonadState и напишу что-нибудь об этом, когда это наконец заработает в моей голове.


person Savanni D'Gerinel    schedule 06.07.2012    source источник
comment
Ах, state более полиморфен, чем я предполагал, потому что по какой-то причине я держал в голове, что эта функция была извинением за то, что конструктор State больше не экспортируется. ТИЛЬ!   -  person Daniel Wagner    schedule 06.07.2012


Ответы (1)


Проблема в том, что вы сделали свои функции append и sumST слишком мономорфными! Вместо прямого использования функции state следует использовать более полиморфные функции get и put, чтобы можно было придать им более интересные типы.

append :: MonadState ST m => Integer -> m ()
append v = do
    ST lst <- get
    put (ST (lst ++ [v]))

sumST :: MonadState ST m => m Integer
sumST = do
    ST lst <- get
    return (sum lst)

Затем вы можете написать именно то myMain, которое вы предложили (хотя вам все равно придется указать начальное состояние в main).

С точки зрения стилистики я бы предложил не определять новый тип ST: существует множество функций, которые делают удобные вещи со списками, и сделать их невозможными для использования, наложив конструктор ST между вами и списками, может раздражать! Если вместо этого вы используете [Integer] в качестве типа состояния, вы можете сделать такие определения:

prepend :: MonadState [Integer] m => Integer -> m ()
prepend = modify . (:)

sumST :: MonadState [Integer] m => m Integer
sumST = gets sum

Выглядит довольно мило, не так ли? знак равно

person Daniel Wagner    schedule 06.07.2012
comment
На самом деле кодирование append и sumST имеет смысл, если вы считаете это упрощением для более сложного типа данных. Как (в моем реальном приложении) ST {datastore::MyData, event_stream::[Events]} - person Savanni D'Gerinel; 06.07.2012