Советы по более элегантному коду с монадами?

Наконец-то я понял, как использовать монады (не знаю, понимаю ли я их...), но мой код никогда не бывает очень элегантным. Я предполагаю, что это из-за отсутствия понимания того, как все эти функции на Control.Monad могут реально помочь. Поэтому я подумал, что было бы неплохо попросить совета по этому поводу в конкретном фрагменте кода, используя монаду состояния.

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

  1. Функция, которая обновляет генератор случайных чисел, имеет тип Seed -> (DeltaPosition, Seed).
  2. Функция, которая обновляет позицию случайного бродяги, имеет тип DeltaPosition -> Position -> (Log, Position) (где Log — это просто способ сообщить мне, какова текущая позиция случайного бродяги).

Я сделал следующее:

У меня есть функция для составления этих двух вычислений с сохранением состояния:

composing :: (g -> (b, g)) -> (b -> s -> (v,s)) -> (s,g) -> (v, (s, g))
composing generate update (st1, gen1) = let (rnd, gen2) = generate gen1
                                            (val, st2)  = update rnd st1
                                        in (val, (st2, gen2))

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

stateComposed :: State g b -> (b -> State s v) -> State (s,g) v
stateComposed rndmizer updater = let generate = runState rndmizer
                                     update x = runState $ updater x
                                 in  State $ composing generate update 

А еще у меня есть самое простое, например, случайный ходок, который просто суммирует случайное число с его текущим положением:

update :: Double -> State Double Double
update x = State (\y -> let z = x+y
                        in  (z,z))

generate :: State StdGen Double
generate = State random

rolling1 = stateComposed generate update 

и функция, чтобы сделать это неоднократно:

rollingN 1 = liftM (:[]) rolling1
rollingN n = liftM2 (:) rolling1 rollings
    where rollings = rollingN (n-1) 

И затем, если я загружу это в ghci и запущу:

*Main> evalState (rollingN 5) (0,mkStdGen 0)
[0.9872770354820595,0.9882724161698186,1.9620425108498993,2.0923229488759123,2.296045158010918]

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

  1. Могу ли я переписать эти функции более «монадным» способом, используя умные функции из Control.Monad?

  2. Существует ли общая схема комбинирования подобных состояний, которую можно использовать? Это как-то связано с преобразователями монад или чем-то подобным?


person Rafael S. Calsaverini    schedule 30.07.2010    source источник
comment
Кстати, рекомендуется избегать использования конструктора данных State, так как в преемнике mtl (monads-fd) State определяется в терминах StateT, и поэтому конструктор данных State не существует.   -  person Travis Brown    schedule 31.07.2010
comment
@TravisBrown На самом деле monads-fd устарел в пользу mtl. (Признавая, что вашему комментарию 5 лет.)   -  person crockeea    schedule 27.10.2015


Ответы (2)


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

takeStep :: (Double, StdGen) -> (Double, StdGen)
takeStep (p, g) = let (d, g') = random g in (p + d, g')

takeSteps n = take n . tail . map fst $ iterate takeStep (0, mkStdGen 0)

Он работает по желанию:

*Main> takeSteps 5
[0.9872770354820595,0.9882724161698186,1.9620425108498993,2.0923229488759123,2.296045158010918]

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

takeStep :: State (Double, StdGen) Double
takeStep = do
  (pos, gen) <- get
  let (delta, gen') = random gen
  let pos' = pos + delta
  put (pos', gen')
  return pos'

takeSteps n = evalState (replicateM n takeStep) (0, mkStdGen 0)

Это дает тот же результат, что и ваш пример:

*Main> takeSteps 5
[0.9872770354820595,0.9882724161698186,1.9620425108498993,2.0923229488759123,2.296045158010918]

Этот подход (выполнение всех манипуляций с состоянием в одной монаде вместо попытки составить State A и State B) кажется мне наиболее элегантным решением.


Обновление: чтобы ответить на ваш вопрос об использовании преобразователей монад для стека State монад: это, безусловно, возможно. Например, мы можем написать следующее:

update' :: (Monad m) => Double -> StateT Double m Double
update' x = StateT $ \y -> let z = x + y in return (z, z)

generate' :: (Monad m) => StateT StdGen m Double
generate' = StateT $ return . random

takeStep' :: StateT Double (State StdGen) Double
takeStep' = update' =<< lift generate'

takeSteps' n = evalState (evalStateT (replicateM n takeStep') 0) $ mkStdGen 0

Мы могли бы также сделать укладку в обратном порядке.

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

person Travis Brown    schedule 30.07.2010

Обычный способ скомпоновать 2 монады (и единственный способ для большинства монад) — это преобразователи монад, но с разными монадами State у вас больше возможностей. Например: вы можете использовать эти функции:

leftState :: State a r -> State (a,b) r
leftState act = state $ \ ~(a,b) -> let
  (r,a') = runState act a
  in (r,(a',b))

rightState :: State b r -> State (a,b) r
rightState act = state $ \ ~(a,b) -> let
  (r,b') = runState act b
  in (r,(a,b'))
person Jeremy List    schedule 27.10.2015