何时使用 Haskell monad
我正在 Haskell 中实现一种组合优化算法:
Given an initial candidate solution, repeat until stopping criteria are met:
1. Determine possible moves
2. Evaluate possible moves
3. Choose a move
4. Make move, record new candidate solution, update search state
我可以为步骤 1-4 编写函数,并将它们链接在一个递归函数中,以处理循环并将状态从一个迭代传递到下一个迭代,但我有一个模糊的想法,即单子适用。
在 Haskell 中表达这种过程的最佳方式是什么?
I'm implementing a combinatorial optimization algorithm in Haskell:
Given an initial candidate solution, repeat until stopping criteria are met:
1. Determine possible moves
2. Evaluate possible moves
3. Choose a move
4. Make move, record new candidate solution, update search state
I could write functions for steps 1-4 and chain them together inside a recursive function to handle looping and passing state from one iteration to the next, but I have a vague idea that monads apply.
What's the best way to express this kind of procedure in Haskell?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
在 Haskell 中表达这种迭代过程的最佳方式是作为每个连续结果的无限列表。将四个步骤拼凑在一起会产生从一个解决方案到另一个(更好)解决方案的函数概念;您所需要做的就是无限多次应用此方法。然后,函数的用户可以使用任何列表函数来获取答案:
solve s0 !! numIterations
,或findstoppingCondition $solve s0
,或任何你想要的。为了到达这里,让我们写出每个函数的类型。
移动::解决方案-> [移动]
给定一个可能的解决方案,找出你可以做出的可能的改变。
值::解决方案->移动->双
给定一个解决方案和一个步骤,对其进行评估并将该值记录为某个实数。
选择::解决方案-> [移动]->移动
给定一个解决方案和一系列动作,选择最好的一个。
应用::解决方案->移动->解决方案
给定一个移动,将其应用到现有的解决方案中以获得新的解决方案。
您想要编写一个具有类似
solve :: Solution -> > 类型的函数。 (解决方案 -> 布尔值) ->解决方案
需要初始解决方案和停止条件来执行算法。相反,让我们把它变成一个无限列表;这意味着您只需删除谓词并获得
Solution ->; [解决方案]
。这里,关键是 iterate :: (a -> a) ->一个-> [a],它重复将函数应用于某个值并给出结果 - 正是算法的描述。
然而,我真正编写的方式如下:
这样做的优点是您可以为任何问题域重用相同的通用结构。您需要做的就是提供
moves
、value
和apply
函数!根据我的心情,我可能会将其重写为:在这里,我们使用应用符号来表示我们实际上只是在执行
(.) apply selectmoves
(这只是apply .选择 $ moves
) 在每个函数都隐式传递参数s
(读者应用程序)的上下文中。如果我们真的想让事情变得简单,我们可以编写任何这些片段都可以完全满足您的需要。 (附带条件:您的任何函数中都没有效果/单子,因此随机性已经消失。不过,您可以轻松地创建这个单子。)
不过,为了好玩,让我们考虑一下
State
单子。这表示某种环境下的计算,因此 State s a 与 s -> 是同构的。 (a,s)——可以查看状态并可能更新它的东西。在这里,函数签名左侧的所有Solution ->
都会消失,->
也会消失。解决方案位于右侧。这将为您留下moves :: State Solution [Move]
value :: Move ->状态解双
choose::[Move] ->状态解决方案移动
apply::Move -> State Solution ()
这意味着您将有一些单子操作
步骤
:您可以使其更加无点,或者如上所述使其具有多态性,但我不会这样做这里。重点是,一旦您有了
step
,您就可以使用runState 生成答案。最后 $replicateM_ numIterations 步骤
,或给定一个whileM
函数,runState $whileM (stoppingCondition :: State Solution Bool) 步骤
。同样,用户可以决定如何停止它。您的moves
和value
函数可能会使用get :: State s s
查询状态;apply
可能会使用modify :: (s -> s) -> State s ()
调整状态而不需要将其拉回。您可以看到这些类型与上面结构的相似之处;事实上,您也可以在step
的定义中看到该结构。每个都说“串在一起apply
、choose
/value
和moves
”,这是您的定义算法。从这两个方面得出的结论是,您希望避免显式循环/递归,正如您正确认识到的那样。如果您以命令式的方式思考此算法,那么 State 单子似乎是一种自然的结构,因为它恰好隐藏了您所想到的那些命令式功能。然而,它也有缺点:例如,一切都变成了单子,而且最糟糕的是,除
apply
之外的函数都能够更改保存的解决方案。如果您将这个算法想象为每次都会产生一个新结果,您就会得到step :: Solution -> 的概念。解决方案
,然后您可以使用iterate
来获取行为良好的无限列表。The best way to express this sort of iterative procedure in Haskell is as an infinite list of each successive result. Piecing together your four steps yields a notion of a function from a solution to a different (better) solution; all you need to do is apply this infinitely many times. The user of your function can then use any list function to get the answer:
solve s0 !! numIterations
, orfind stoppingCondition $ solve s0
, or whatever you want.In order to get here, let's write out the types for each of these functions.
moves :: Solution -> [Move]
Given a possible solution, figure out the possible changes you can make.
value :: Solution -> Move -> Double
Given a solution and a move, evaluate it and record that value as some real number.
choose :: Solution -> [Move] -> Move
Given a solution and a list of moves, pick the best one.
apply :: Solution -> Move -> Solution
Given a move, apply it to an existing solution to get a new one.
You want to write a function with a type something like
solve :: Solution -> (Solution -> Bool) -> Solution
which takes an initial solution and a stopping condition to execute your algorithm.Instead, let's make this an infinite list; this means that you'll just remove the predicate and have
Solution -> [Solution]
.Here, the key is
iterate :: (a -> a) -> a -> [a]
, which repeatedly applies a function to a value and gives you the results—exactly the description of your algorithm.However, the way I'd really write this would be the following:
The advantage of this is that you can reuse this same generic structure for any problem domain. All you need to do is to provide the
moves
,value
, andapply
functions! And depending on my mood, I might rewrite that as this:Here, we use applicative notation to say that we're effectively just doing
(.) apply choose moves
(which is justapply . choose $ moves
) in a context where each of those functions is implicitly passed a parameters
(the reader applicative). If we really wanted to tersify things, we could writeAny of these snippets will do exactly what you need. (Proviso: there are no effects/monads in any of your functions, so randomness is out. You make this monadic easily, though.)
Just for kicks, though, let's think about the
State
monad. This represents a computation with some sort of environment, so thatState s a
is isomorphic tos -> (a,s)
—something which can see the state and potentially update it. Here, all theSolution ->
s on the left of your function signatures would disappear, as would the-> Solution
s on the right. That would leave you withmoves :: State Solution [Move]
value :: Move -> State Solution Double
choose :: [Move] -> State Solution Move
apply :: Move -> State Solution ()
This means that you would have some monadic action
step
:You could make this more point-free, or make it polymorphic just as above, but I won't do that here. The point is that once you have
step
, you can generate answers withrunState . last $ replicateM_ numIterations step
, or given awhileM
function,runState $ whileM (stoppingCondition :: State Solution Bool) step
. Again, the user can decide how to stop it. Yourmoves
andvalue
functions would probably query the state withget :: State s s
;apply
would probably usemodify :: (s -> s) -> State s ()
to tweak the state without needing to pull it back out. You can see the similarity with the structure from above in these types; and in fact, you can see that structure in the definition ofstep
, as well. Each one says "string togetherapply
,choose
/value
, andmoves
", which is the definition of your algorithm.The take-home message from both of these is that you want to avoid explicit loops/recursion, as you so rightly realized. If you think about this algorithm imperatively, then the
State
monad seems like a natural structure, as it hides exactly those imperative features you were thinking of. However, it has downsides: for instance, everything has become monadic, and—worst of all—functions other thanapply
are able to change the saved solution. If you instead imagine this algorithm as producing a new result each time, you get the notion ofstep :: Solution -> Solution
, and from there you can useiterate
to get a well-behaved infinite list.这是如何使用 的伪代码草图
State
monad 通过计算线程化搜索状态:请注意,即使您的主要计算位于状态 monad 中,但并非每个组件都必须位于 monad 中。在这里,
evaluateMoves
和chooseMove
是非单子的,我使用let
向您展示如何将它们显式集成到do
块。不过,一旦您习惯了这种风格,您可能会想要习惯使用<$>
(又名fmap
)和函数组合来变得更加简洁:Here's a pseudocodey sketch of how you might use the
State
monad to thread the search state through the computation:Notice that even though your main computation is in the state monad, not every component has to be in the monad. Here,
evaluateMoves
andchooseMove
are non-monadic, and I've usedlet
to show you how to explicitly integrate them into ado
block. Once you get comfortable with this style, though, you'll probably want to get comfortable using<$>
(akafmap
) and function composition to get more succinct: