Советы для более элегантного кода с монадами?
Наконец-то я понял, как использовать монады (не знаю, понимаю ли я их...), но мой код никогда не бывает очень изящным. Полагаю, это из-за отсутствия хватки в том, как действительно могут помочь все эти функции на Control.Monad
. Поэтому я подумал, что было бы неплохо попросить совета об этом в определенном фрагменте кода, используя государственную монаду.
Цель кода - рассчитать множество видов случайных блужданий, и это то, что я пытаюсь сделать перед чем-то более сложным. Проблема в том, что у меня одновременно есть два вычисления с учётом состояния, и я хотел бы знать, как скомбинировать их с элегантностью:
- Функция, которая обновляет генератор случайных чисел, имеет тип
Seed -> (DeltaPosition, Seed)
- Функция, которая обновляет положение случайного ходока, является чем-то вроде типа
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]
Я получаю то, что хочу, это список позиций, занимаемых случайным ходоком. Но... я чувствую, что должен быть более элегантный способ сделать это. У меня есть два вопроса:
-
Могу ли я переписать эти функции более "монадическим" способом, используя умные функции из Control.Monad
?
-
Существует ли общая схема объединения таких состояний, которые можно использовать? Связано ли это с монадными трансформаторами или что-то в этом роде?
Ответы
Ответ 1
Обновление: я должен был упомянуть, что на самом деле гораздо лучший способ сделать это, который не требует State
или monads вообще:
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
monads: это, безусловно, возможно. Мы можем написать следующее, например:
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
немного понятна.
Ответ 2
Обычный способ составления 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'))