Как я могу инициализировать состояние скрытым способом в Haskell (как это делает PRNG)?

Я прошел несколько руководств по монаде State и, кажется, уловил идею.

Например, как в этом замечательном руководстве:

import Data.Word

type LCGState = Word32

lcg :: LCGState -> (Integer, LCGState)
lcg s0 = (output, s1) 
  where s1 = 1103515245 * s0 + 12345
        output = fromIntegral s1 * 2^16 `div` 2^32


getRandom :: State LCGState Integer
getRandom = get >>= \s0 -> let (x,s1) = lcg s0
                           in put s1 >> return x

Итак, я могу использовать getRandom:

*Main> runState getRandom 0
(0,12345)
*Main> runState getRandom 0
(0,12345)
*Main> runState getRandom 1              
(16838,1103527590)

Но мне все еще нужно передавать семя в PRNG каждый раз, когда я его вызываю. Я знаю, что PRNG, доступный в реализациях Haskell, не нуждается в этом:

Prelude> :module Random
Prelude Random> randomRIO (1,6 :: Int)
(...) -- GHC prints some stuff here
6
Prelude Random> randomRIO (1,6 :: Int)
1

Так что я, вероятно, неправильно понял монаду State, потому что то, что я мог видеть в большинстве руководств, похоже, не является «постоянным» состоянием, а просто удобным способом для состояния потока.

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

Большое спасибо!


person Jay    schedule 13.07.2009    source источник


Ответы (3)


randomRIO использует монаду IO. Кажется, это хорошо работает в интерпретаторе, потому что интерпретатор также работает в монаде IO. Это то, что вы видите в своем примере; на самом деле вы не можете сделать это на верхнем уровне в коде - вам все равно придется поместить это в выражение do, как и все монады.

В общем коде вам следует избегать монады IO, потому что, как только ваш код использует монаду IO, он навсегда привязывается к внешнему состоянию — вы не можете выйти из него (т. е. если у вас есть код, который использует монаду IO, любой код тот, кто его вызывает, также должен использовать монаду IO; нет безопасного способа «выйти» из нее). Таким образом, монада IO должна использоваться только для таких вещей, как доступ к внешней среде, где это абсолютно необходимо.

Для таких вещей, как локальное автономное состояние, вы не должны использовать монаду IO. Вы можете использовать монаду State, как вы упомянули, или вы можете использовать монаду ST. Монада ST во многом похожа на монаду IO; т.е. имеется STRef изменяемых ячеек, аналогичных IORef. Прелесть ST по сравнению с IO заключается в том, что когда вы закончите, вы можете вызвать runST для монады ST, чтобы получить результат вычислений из монады, чего вы не можете сделать с IO.

Что касается «скрытия» состояния, то это просто часть синтаксиса do-выражений в Haskell для монад. Если вы считаете, что вам нужно явно передать состояние, то вы неправильно используете синтаксис монады.

Вот код, который использует IORef в IO Monad:

import Data.IORef
foo :: IO Int -- this is stuck in the IO monad forever
foo = do x <- newIORef 1
         modifyIORef x (+ 2)
         readIORef x
-- foo is an IO computation that returns 3

Вот код, использующий монаду ST:

import Control.Monad.ST
import Data.STRef
bar :: Int
bar = runST (do x <- newSTRef 1
                modifySTRef x (+ 2)
                readSTRef x)
-- bar == 3

Простота кода по существу такая же; за исключением того, что в последнем случае мы можем получить значение из монады, а в первом мы не можем, не поместив его в другое вычисление ввода-вывода.

person newacct    schedule 13.07.2009
comment
Спасибо! Это было очень полезно. Что касается вашего комментария, в первом случае мы не можем не поместить его в другое вычисление ввода-вывода - я полагаю, поэтому монада ввода-вывода должна быть самой внутренней, когда вы объединяете несколько монад (используя преобразователи монад), тогда? - person Jay; 14.07.2009
comment
Теперь, когда я думаю об этом, PRNG обязательно будет использовать монаду IO, верно? Ему нужна энтропия, поэтому откуда-то потребуются данные. На самом деле ему нужна по крайней мере одна функция, которая не является ссылочно прозрачной (иначе она не была бы полезна, например, для криптографического кода)! :-) - person Jay; 14.07.2009

secretStateValue :: IORef SomeType
secretStateValue = unsafePerformIO $ newIORef initialState
{-# NOINLINE secretStateValue #-}

Теперь получите доступ к вашему secretStateValue с помощью обычных readIORef и writeIORef, в монаде IO.

person bdonlan    schedule 13.07.2009
comment
Говоря об этом, Random использует atomicModifyIORef вместо пары readIORef + writeIORef; это кажется хорошей идеей. - person ephemient; 13.07.2009
comment
Действительно, если будет доступ из нескольких потоков, лучше использовать atomicModifyIORef. - person bdonlan; 14.07.2009

Так что я, вероятно, неправильно понял монаду State, потому что то, что я мог видеть в большинстве руководств, похоже, не является «постоянным» состоянием, а просто удобным способом для состояния потока.

Монада состояния как раз и предназначена для передачи состояния через некоторую область видимости.

Если вам нужно состояние верхнего уровня, это вне языка (и вам придется использовать глобальную изменяемую переменную). Обратите внимание, как это, вероятно, усложнит потокобезопасность вашего кода — как инициализируется это состояние? и когда? И какой нитью?

person Don Stewart    schedule 13.07.2009