Государственная монада, последовательности случайных чисел и монадический код

Я пытаюсь понять Монаду Состояния, и с этой целью я хотел написать монадический код, который генерировал бы последовательность случайных чисел с помощью Линейного Конгруэнтного Генератора (вероятно, не очень хорошо, но я намерен просто изучить Монаду Состояния, а не построить хорошую библиотеку ГСЧ).

Генератор просто такой (для простоты я хочу сгенерировать последовательность из Bools):

type Seed = Int

random :: Seed -> (Bool, Seed)
random seed = let (a, c, m) = (1664525, 1013904223, 2^32)  -- some params for the LCG
                  seed' = (a*seed + c) `mod` m
              in  (even seed', seed')   -- return True/False if seed' is even/odd 

Не беспокойтесь о числах, это просто правило обновления для начального числа, которое (согласно числовым рецептам) должно генерировать псевдослучайную последовательность Ints. Теперь, если я хочу генерировать случайные числа последовательно, я бы сделал:

rand3Bools :: Seed -> ([Bool], Seed)
rand3Bools seed0  = let (b1, seed1) = random seed0
                        (b2, seed2) = random seed1
                        (b3, seed3) = random seed2
                    in  ([b1,b2,b3], seed3)

Хорошо, поэтому я мог бы избежать этого шаблона, используя Монаду состояния:

import Control.Monad.State

data Random {seed :: Seed, value :: Bool}

nextVal = do 
   Random seed val <- get 
   let seed' = updateSeed seed
       val'  = even seed'
   put (Random seed' val')
   return val'

updateSeed seed = let (a,b,m) = (1664525, 1013904223, 2^32) in (a*seed + c) `mod` m

И наконец:

getNRandSt n = replicateM n nextVal 

getNRand :: Int -> Seed -> [Bool]
getNRand   n seed = evalState (getNRandStates n) (Random seed True)

Хорошо, это работает нормально, и дайте мне список из n псевдослучайных Bool для каждого заданного числа. Но...

Я могу прочитать, что я сделал (в основном на основе этого примера: http://www.haskell.org/pipermail/beginners/2008-September/000275.html) и воспроизвести его для других целей. Но я не думаю, что могу понять, что на самом деле происходит за do-notation и монадическими функциями (например, replicateM).

Может ли кто-нибудь помочь мне с некоторыми из этих сомнений?

1 - Я пытался обессахаривать функцию nextVal, чтобы понять, что она делает, но не смог. Я могу предположить, что он извлекает текущее состояние, обновляет его, а затем передает состояние следующему вычислению, но это просто основано на чтении этого do-sugar, как если бы оно было английским.

Как мне действительно удалить сахар из этой функции в исходную >> = и пошагово вернуть функции?

2 - Я не мог понять, что именно делают функции put и get. Могу догадаться, что они «пакуют» и «распаковывают» государство. Но механика, лежащая в основе до-сахара, для меня все еще неуловима.

Что ж, любые другие общие замечания об этом коде очень приветствуются. Иногда мне казалось, что с Haskell я могу создать код, который работает и делать то, что я от него ожидаю, но я не могу «следить за оценкой», как я привык делать с императивными программами.


person Rafael S. Calsaverini    schedule 24.12.2009    source источник


Ответы (3)


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

Во-первых, State имеет два параметра типа: тип содержащихся данных состояния и тип окончательного результата вычисления. Здесь мы будем использовать stateData и result соответственно в качестве переменных типа для них. Если задуматься, это имеет смысл; Определяющая характеристика вычислений на основе состояний состоит в том, что они изменяют состояние при создании вывода.

Менее очевидно то, что конструктор типа принимает функцию из состояния в измененное состояние и результат, например:

newtype State stateData result = State (stateData -> (result, stateData))

Таким образом, хотя монада называется «Состояние», фактическое значение, заключенное в оболочку монады, - это значение вычисления на основе состояния, а не фактическое значение содержащегося в нем состояния.

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

runState (State f) = f

Итак, что это означает, когда вы определяете функцию, которая возвращает значение состояния? Давайте на мгновение проигнорируем тот факт, что State - это монада, и просто посмотрим на лежащие в основе типы. Сначала рассмотрим эту функцию (которая на самом деле ничего не делает с состоянием):

len2State :: String -> State Int Bool
len2State s = return ((length s) == 2)

Если вы посмотрите на определение состояния, мы увидим, что здесь тип stateData - это Int, а тип result - это Bool, поэтому функция, заключенная в конструктор данных, должна иметь тип Int -> (Bool, Int). А теперь представьте версию _12 _ без состояния - очевидно, что она будет иметь тип String -> Bool. Итак, как бы вы преобразовали такую ​​функцию в функцию, возвращающую значение, которое помещается в оболочку State?

Что ж, очевидно, преобразованная функция должна будет принимать второй параметр, Int, представляющий значение состояния. Также необходимо вернуть значение состояния, другое Int. Поскольку на самом деле мы ничего не делаем с состоянием в этой функции, давайте просто сделаем очевидную вещь - передадим этот int прямо до конца. Вот функция в форме состояния, определенная в терминах версии без состояния:

len2 :: String -> Bool
len2 s = ((length s) == 2)

len2State :: String -> (Int -> (Bool, Int))
len2State s i = (len2' s, i)

Но это отчасти глупо и излишне. Давайте обобщим преобразование, чтобы мы могли передать значение результата и превратить что угодно в функцию, подобную состоянию.

convert :: Bool -> (Int -> (Bool, Int))
convert r d = (r, d)

len2 s = ((length s) == 2)

len2State :: String -> (Int -> (Bool, Int))
len2State s = convert (len2 s)

Что, если нам нужна функция, изменяющая состояние? Очевидно, мы не можем создать его с convert, поскольку мы написали это для передачи состояния. Давайте сохраним простоту и напишем функцию для перезаписи состояния новым значением. Какой тип ему нужен? Ему понадобится Int для нового значения состояния, и, конечно же, он должен будет вернуть функцию stateData -> (result, stateData), потому что это то, что нужно нашей оболочке State. Перезапись значения состояния на самом деле не имеет разумного значения result вне вычисления состояния, поэтому нашим результатом здесь будет просто (), кортеж с нулевым элементом, который представляет "отсутствие значения" в Haskell.

overwriteState :: Int -> (Int -> ((), Int))
overwriteState newState _ = ((), newState)

Это было просто! Теперь давайте на самом деле что-нибудь с этими данными состояния. Давайте перепишем len2State сверху во что-то более разумное: сравним длину строки с текущим значением состояния.

lenState :: String -> (Int -> (Bool, Int))
lenState s i = ((length s) == i, i)

Можем ли мы обобщить это на преобразователь и функцию без состояния, как мы делали раньше? Не так просто. Наша len функция должна будет принимать состояние в качестве аргумента, но мы не хотим, чтобы она «знала о» состоянии. Действительно, неудобно. Однако мы можем написать быструю вспомогательную функцию, которая обрабатывает все за нас: мы дадим ей функцию, которая должна использовать значение состояния, и она передаст значение, а затем упакует все обратно в функцию в форме состояния. оставив len никого не мудрее.

useState :: (Int -> Bool) -> Int -> (Bool, Int)
useState f d = (f d, d)

len :: String -> Int -> Bool
len s i = (length s) == i

lenState :: String -> (Int -> (Bool, Int))
lenState s = useState (len s)

А теперь самое сложное - что, если мы захотим связать эти функции вместе? Допустим, мы хотим использовать lenState в строке, затем удвоить значение состояния, если результат ложный, затем снова проверить строку и, наконец, вернуть истину, если какая-либо из проверок прошла. У нас есть все детали, необходимые для этой задачи, но записывать все это было бы затруднительно. Можем ли мы создать функцию, которая автоматически объединяет две функции, каждая из которых возвращает функции, подобные состоянию? Конечно! Нам просто нужно убедиться, что он принимает в качестве аргументов две вещи: функцию состояния, возвращаемую первой функцией, и функцию, которая принимает в качестве аргумента тип result предыдущей функции. Посмотрим, как получится:

chainStates :: (Int -> (result1, Int)) -> (result1 -> (Int -> (result2, Int))) -> (Int -> (result2, Int))
chainStates prev f d = let (r, d') = prev d
                       in f r d'

Все, что это делает, - это применение первой функции состояния к некоторым данным состояния, а затем применение второй функции к результату и измененным данным состояния. Все просто, правда?

А теперь самое интересное: между chainStates и convert мы почти сможем превратить любую комбинацию функций без состояния в функцию с поддержкой состояния! Единственное, что нам сейчас нужно, это замена useState, который возвращает данные состояния в качестве своего результата, так что chainStates может передавать их функциям, которые ничего не знают о трюке, который мы им навязываем. Кроме того, мы будем использовать лямбды, чтобы принять результат от предыдущих функций и дать им временные имена. Хорошо, давайте сделаем это:

extractState :: Int -> (Int, Int)
extractState d = (d, d)

chained :: String -> (Int -> (Bool, Int))
chained str = chainStates  extractState         $ \state1 ->
              let check1 = (len str state1) in
              chainStates (overwriteState (
                  if check1 
                  then state1 
                  else state1 * 2))             $ \ _ ->
              chainStates  extractState         $ \state2 ->
              let check2 = (len str state2) in
              convert (check1 || check2)

И попробуйте:

> chained "abcd" 2
(True, 4)
> chained "abcd" 3
(False, 6)
> chained "abcd" 4
(True, 4)
> chained "abcdef" 5
(False, 10)

Конечно, мы не можем забывать, что State - это на самом деле монада, которая обертывает функции, подобные State, и удерживает нас от них, поэтому ни одна из наших изящных функций, которые мы создали, не поможет нам с реальными вещами. Или они? В шокирующем повороте оказалось, что настоящая монада State предоставляет все те же функции под разными именами:

runState (State s) = s
return r = State (convert r)
(>>=) s f = State (\d -> let (r, d') = (runState s) d in
                         runState (f r) d')
get = State extractState
put d = State (overwriteState d)

Обратите внимание, что >> = почти идентично chainStates, но не было хорошего способа определить его с помощью chainStates. Итак, чтобы подвести итог, мы можем переписать последний пример, используя реальное состояние:

chained str = get                               >>= \state1 ->
              let check1 = (len str state1) in
              put (if check1 
                  then state1 else state1 * 2)  >>= \ _ ->
              get                               >>= \state2 ->
              let check2 = (len str state2) in
              return (check1 || check2)

Или все засахаренные эквивалентной записью:

chained str = do
        state1 <- get
        let check1 = len str state1
        _ <- put (if check1 then state1 else state1 * 2)
        state2 <- get
        let check2 = (len str state2)
        return (check1 || check2)
person C. A. McCann    schedule 24.12.2009
comment
Спасибо, мне тоже было приятно писать, хотя, возможно, в отдельных местах потребовалось бы больше работы. Я волновался, что людей могла оттолкнуть длина, но я думаю, что нет ... - person C. A. McCann; 25.12.2009

Прежде всего, ваш пример слишком сложен, потому что ему не нужно хранить val в монаде состояний; только семя - это постоянное состояние. Во-вторых, я думаю, вам повезет больше, если вместо использования стандартной монады состояний вы сами повторно реализуете всю монаду состояний и ее операции с их типами. Думаю, так вы узнаете больше. Вот несколько заявлений, с которых можно начать:

data MyState s a = MyState (s -> (s, b))

get :: Mystate s s
put :: s -> Mystate s ()

Затем вы можете написать свои собственные связки:

unit :: a -> Mystate s a
bind :: Mystate s a -> (a -> Mystate s b) -> Mystate s b

Ну наконец то

data Seed = Seed Int
nextVal :: Mystate Seed Bool

Что касается вашей проблемы с обессахариванием, используемая вами нотация do довольно сложна. Но обессахаривание - это поэтапная механическая процедура. Насколько я понимаю, ваш код должен десахарироваться следующим образом (возвращаясь к вашим исходным типам и коду, с которыми я не согласен):

 nextVal = get >>= \ Random seed val ->
                      let seed' = updateSeed seed
                          val'  = even seed'
                      in  put (Random seed' val') >>= \ _ -> return val'

Чтобы сделать структуру вложения немного более ясной, я позволил себе большую вольность с отступом.

person Norman Ramsey    schedule 24.12.2009

У тебя есть пара отличных отзывов. Когда я работаю с монадой State, я думаю, заменяю State s a на s -> (s,a) (в конце концов, это действительно то, что есть).

Затем вы получите тип привязки, который выглядит так:

(>>=) :: (s -> (s,a)) ->
         (a -> s -> (s,b)) ->
         (s -> (s,b))

и вы видите, что bind - это просто особый вид оператора композиции функций, например (.)

Я написал блог / руководство по монаде состояний здесь. Возможно, он не особенно хорош, но помог мне немного лучше разобраться, написав его.

person jberryman    schedule 25.12.2009
comment
Гиперссылка на ваш блог не работает; есть ли обновленный вариант, которым вы хотели бы поделиться с нами? - person Ruud Helderman; 19.08.2018