Государственная монада, последовательности случайных чисел и монадический код - PullRequest
17 голосов
/ 24 декабря 2009

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

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

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 

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

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 s для каждого заданного семени. Но ...

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

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

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

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

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

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

Ответы [ 3 ]

31 голосов
/ 24 декабря 2009

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

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

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

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

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

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

runState (State f) = f

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

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

Если вы посмотрите на определение State, мы увидим, что здесь тип stateData равен Int, а тип result равен Bool, поэтому функция, заключенная в конструктор данных, должна иметь тип Int -> (Bool, Int). Теперь представьте версию len2State без состояния - очевидно, она будет иметь тип 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), потому что это то, что нужно нашему упаковщику состояний. Перезапись значения состояния на самом деле не имеет разумного значения result за пределами вычисления State, поэтому наш результат здесь будет просто (), кортеж с нулевым элементом, который представляет "нет значения" в 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 для строки, затем удвоить значение состояния, если результат равен false, затем проверить строку еще раз и, наконец, вернуть true, если какая-либо проверка прошла. У нас есть все части, которые нам нужны для этой задачи, но написать все это было бы больно. Можем ли мы создать функцию, которая автоматически связывает воедино две функции, каждая из которых возвращает функции, подобные состоянию? Конечно, вещь! Нам просто нужно убедиться, что он принимает в качестве аргументов две вещи: функцию State, возвращаемую первой функцией, и функцию, которая принимает тип 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, и удерживает нас от них, поэтому ни одна из наших изящных функций, которые мы создали, не поможет нам с реальной вещью. Или они будут? В шокирующем повороте оказывается, что настоящая государственная монада предоставляет все те же функции под разными именами:

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. Итак, чтобы подвести итоги, мы можем переписать последний пример, используя real State:

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)
8 голосов
/ 24 декабря 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'

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

5 голосов
/ 25 декабря 2009

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

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

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

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

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

...