使用 monad 获得更优雅代码的技巧?

发布于 2024-09-12 09:17:36 字数 1995 浏览 6 评论 0原文

我终于掌握了如何使用 monad(不知道我是否理解它们......),但我的代码从来都不是很优雅。我猜想是因为缺乏对 Control.Monad 上的所有这些功能如何真正发挥作用的了解。因此,我认为最好在使用状态单子的特定代码段中寻求有关此问题的提示。

代码的目标是计算多种随机游走,这是我在更复杂的事情之前尝试做的事情。问题是我同时有两个有状态计算,我想知道如何优雅地组合它们:

  1. 更新随机数生成器的函数是 Seed ->; 类型的函数。 (DeltaPosition, Seed)
  2. 更新随机游走者位置的函数是 DeltaPosition -> 类型的函数。位置-> (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. 是否存在可以使用的关于组合这样的状态的通用模式?这与 monad 转换器或类似的东西有关吗?

I finally got a hold on how to use monads (don't know if I understand them...), but my code is never very elegant. I guess is from a lack of grip on how all those functions on Control.Monad can really help. So I'd thought it would be nice to ask for tips on this in a particular piece of code using the state monad.

The goal of the code is to calculate many kinds of random walks, and it's something I'm trying to do before something more complicated. The problem is that I have two stateful computations at the same time, and I'd like to know how to compose them with elegance:

  1. The function that updates the random number generator is something of type Seed -> (DeltaPosition, Seed)
  2. The function that updates the position of the random walker is something of type DeltaPosition -> Position -> (Log, Position) (where Log is just some way for me to report what is the current position of the random walker).

What I've done is this:

I have a function to compose this two stateful computations:

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))

and then I turn it into a function that compose states:

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 

And then I have the simplest thing, for example, a random walker that will just sum a random number to its current position:

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 

and a function to do this repeatedly:

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

And then, if I load this in ghci and run:

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

I get what I want, which is a list of the positions occupied by the random walker. But... I feel there must be a more elegant way to do this. I have two questions:

  1. Can I rewrite those functions in a more "monadic" way, using clever functions from Control.Monad?

  2. Is there a general pattern about combining states like this that can be used? Does this have something to do with monad transformers or something like that?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

各自安好 2024-09-19 09:17:36

更新:我应该提到,实际上有一种更好的方法来做到这一点,根本不需要 State 或 monad:

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]

这种方法(在单个 monad 中执行所有状态操作,而不是尝试组合 State A< /code> 和 State B) 在我看来是最优雅的解决方案。


更新:回答有关使用 monad 转换器堆叠 State monad 的问题:这当然是可能的。例如,我们可以编写以下内容:

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 版本更清晰一些。

Update: I should have mentioned that there's actually a much nicer way to do this that doesn't require State or monads at all:

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)

It works as desired:

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

If you're not committed to the idea of "composing" two separate stateful computations, you can accomplish the same thing much more straightforwardly:

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)

This produces the same output as your example:

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

This approach (doing all the state manipulation in a single monad instead of trying to compose a State A and State B) seems to me like the most elegant solution.


Update: To answer your question about using monad transformers to stack State monads: it's certainly possible. We can write the following, for example:

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

We could also do the stacking in the opposite order.

This version again produces the same output, but in my opinion the non-StateT version is a bit clearer.

几度春秋 2024-09-19 09:17:36

组合 2 个 monad 的常用方法(也是大多数 monad 的唯一方法)是使用 monad 转换器,但对于不同的 State monad,您有更多选择。例如:您可以使用这些函数:

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'))

The usual way to compose 2 monads (and the only way for most monads) is with monad transformers, but with different State monads you have more options. For example: you could use these functions:

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'))
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文