Можно ли эмулировать поведение StateT без использования пользовательских типов?

Если у нас есть следующие две функции, сложение и вычитание, их просто связать в цепочку, чтобы выполнить серию вычислений на входе:

add :: Int -> State Int ()
add n = state $ \x -> ((),x+n)

subtract :: Int -> State Int ()
subtract n = state $ \x -> ((),x-n)

manyOperations :: State Int ()
manyOperations = do
    add 2
    subtract 3
    add 5
    --etc


result = execState manyOperations 5
--result is 9

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

add :: Int -> StateT Int IO ()
add n = StateT $ \x -> print (x+n) >> return ((),x+n)

subtract :: Int -> StateT Int IO ()
subtract n = StateT $ \x -> print (x-n) >> return ((),x-n)

manyOperations :: StateT Int IO ()
manyOperations = do
    add 2
    subtract 3
    add 5

main = runStateT manyOperations 5
-- prints 7, then 4, then 9

Можно ли воспроизвести это «комбинированное вычисление и печать» без StateT или каких-либо пользовательских типов данных?

Насколько я знаю, можно выполнять все процессы, которые MaybeT IO a может выполнять, используя IO (Maybe a), но похоже, что это просто вложенные монады. С другой стороны, у StateT может не быть альтернативы, потому что s -> (a,s) принципиально отличается от s -> m (a,s).

Я вижу только два направления, в которых может пойти код: использование State Int (IO ()) или IO (State Int ()), но оба они кажутся бессмысленными, учитывая реализацию StateT.

Я считаю, что это невозможно. Я прав?

Примечание. Я знаю, что это совершенно непрактично, но я не смог найти решения после нескольких часов работы, что означает, что я прав или моих навыков недостаточно для выполнения задачи.


person lightandlight    schedule 30.01.2015    source источник
comment
Нет ничего невозможного... но зачем тебе это? Для этого для предназначен StateT. Вы всегда можете просто обойти состояние Int явно и делать все только в IO ()...   -  person crockeea    schedule 30.01.2015
comment
Вы можете полностью избежать этих типов данных, используя s -> IO (a, s) напрямую, заменяя a и s соответствующим образом. Хотя это точно будет не так красиво.   -  person David Young    schedule 30.01.2015
comment
Также стоит отметить, что State определяется через StateT, а не наоборот.   -  person David Young    schedule 30.01.2015
comment
@DavidYoung Превратите это в ответ!   -  person Daniel Wagner    schedule 31.01.2015


Ответы (3)


Конечно, вы можете сделать всю сантехнику самостоятельно. Монады не добавляют ничего нового в Haskell, они просто позволяют повторно использовать массу кода и сокращать шаблоны. Так что все, что вы можете сделать с монадой, вы можете с трудом сделать вручную.

manyOperations :: Int -> IO ()
manyOperations n0 = do
    let n1 = n0 + 2
    print n1
    let n2 = n1 - 3
    print n2
    let n3 = n2 + 5
    print n3

Если количество повторений в приведенной выше функции слишком велико для вас (это для меня!), вы можете попытаться уменьшить его с помощью функции «комбинированных вычислений и печати», но на данный момент вы наклоняясь назад, чтобы избежать StateT.

-- a bad function, never write something like this!
printAndCompute :: a -> (a -> b) -> IO b
printAndCompute a f = let b = f a in print b >> return b

-- seriously, don't do this!
manyOperations n0 = do
    n1 <- printAndCompute (+2) n0
    n2 <- printAndCompute (-3) n1
    n3 <- printAndCompute (+5) n2
    return ()
person cdk    schedule 30.01.2015

Я не уверен, что это именно то, что вы ищете, но вы можете определить операцию, которая берет операцию с отслеживанием состояния, которая у вас уже есть, и распечатывает состояние после выполнения операции -

withPrint :: (Show s) => State s a -> StateT s IO a
withPrint operation = do
    s <- get
    let (a, t) = runState operation s
    liftIO (print t)
    put t
    return a

а затем сделать

manyOperations :: StateT Int IO ()
manyOperations = do
    withPrint (add 2)
    withPrint (subtract 3)
    withPrint (add 5)
    -- etc

Это не избегает использования StateT, но абстрагирует преобразование ваших обычных операций с состоянием, не связанных с вводом-выводом, в операции с полным вводом-выводом, поэтому вам не нужно беспокоиться о деталях (кроме добавления withPrint когда вы хотите, чтобы состояние было напечатано.

Если вы не хотите, чтобы состояние печаталось для конкретной операции, вы можете определить

withoutPrint :: State s a -> StateT s IO a
withoutPrint operation = do
    s <- get
    let (a, t) = runState operation s
    put t
    return a

что на самом деле эквивалентно

import Control.Monad.Morph

withoutPrint :: State s a -> StateT s IO a
withoutPrint = hoist (\(Identity a) -> return a)

используя hoist из Control.Monad.Morph для преобразования монады, лежащей в основе StateT, из Identity в IO.

person Chris Taylor    schedule 30.01.2015
comment
Абстрагирование печати может быть полезной функцией, но это не то, что мне нужно. Тем не менее, спасибо за вклад. - person lightandlight; 31.01.2015

Вы можете полностью избежать этих типов данных, используя s -> IO (a, s) напрямую, заменяя a и s соответствующим образом. Хотя это точно будет не так красиво.

Это будет выглядеть примерно так:

-- This makes StateIO s act as a shorthand for s -> IO (a, s)
type StateIO s a = s -> IO (a, s)

add :: Int -> StateIO Int ()
add n = \x -> print (x+n) >> return ((),x+n)

sub :: Int -> StateIO Int ()
sub n = \x -> print (x-n) >> return ((),x-n)

manyOperations :: StateIO Int ()
manyOperations =   -- Using the definition of (>>=) for StateT
  \s1 -> do        -- and removing a lambda that is immediately applied
      (a, s2) <- add 2 s1
      (a, s3) <- sub 3 s2
      add 5 s3

result :: IO ((), Int)
result = manyOperations 5

Состояние должно быть явно пропущено через все операции. Это главное преимущество, которое мы получаем от использования типа данных StateT: метод (>>=) его экземпляра Monad делает все это за нас! Однако из этого примера видно, что в StateT нет ничего волшебного. Это просто очень хороший способ абстрагироваться от потока состояния.

Кроме того, отношения между StateT и State противоположны отношениям между MaybeT и Maybe: State определяется в терминах StateT. Мы видим, что этот факт выражен в синоним этого типа:

type State s = StateT s Identity

Это преобразователь над Identity type (который имеет тривиальный экземпляр Monad).

person David Young    schedule 31.01.2015