你如何使用 list monad 来计算/表示非确定性计算的结果?
Posted
技术标签:
【中文标题】你如何使用 list monad 来计算/表示非确定性计算的结果?【英文标题】:How do you use the list monad to compute/represent the outcome of a non-deterministic computation? 【发布时间】:2013-06-18 14:11:12 【问题描述】:我想构建一个计算,其中上下文是引导现在的所有路径的历史(形成一棵树),而函数是当前状态,以过去状态为条件。该函数本身是不确定的,因此一个过去的状态可能会导致几个未来的状态,因此是树分支。将此计算的结果表示为一棵树是有意义的,但是有没有办法用 list monad 简洁地表达它?还是我不知道的其他构造?
【问题讨论】:
你认为你可以使用列表推导来清楚地表达你的功能吗? list monad 几乎相同,但语法略有不同。 【参考方案1】:我想补充一下 Tikhon Jelvis 的回答,如果您需要跟踪执行分支的方式,您可以使用更复杂的 monad 堆栈组合。例如:
import Control.Monad
import Control.Monad.Writer
import Data.Sequence
-- | Represents a non-deterministic computation
-- that allows to trace the execution by sequences of 'w'.
type NonDet w a = WriterT (Seq w) [] a
WriterT (Seq w) [] a
的值在[(a, Seq w)]
内部,即可能结果的列表,每个结果都与w
类型的标记序列一起保存。我们使用这些标记来追踪我们的步骤。
我们首先创建一个辅助函数,它只是为当前的执行轨迹添加一个标记:
-- | Appends a mark to the current trace.
mark :: w -> NonDet w ()
mark = tell . singleton
也许还有一个更方便的函数,它添加一个标记,然后继续进行给定的计算:
-- | A helper function appends a mark and proceeds.
(#>) :: w -> NonDet w a -> NonDet w a
(#>) x = (mark x >>)
作为一个非常简单的例子,假设我们要遍历一棵树
data Tree a = Leaf a | Bin (Tree a) (Tree a)
(实际上,当然不会有树,分支将由复杂的东西决定。)
我们会记住我们使用一系列方向走过的路径
data Direction = L | R
deriving (Show, Read, Eq, Ord, Enum, Bounded)
我们的遍历函数将如下所示:
traverse :: Tree a -> NonDet Direction a
traverse (Leaf x) = return x
traverse (Bin l r) = (L #> traverse l) `mplus` (R #> traverse r)
打电话
runWriterT $ traverse $ Bin (Bin (Leaf "a") (Leaf "b")) (Leaf "c")
产生于
[("a",fromList [L,L]),("b",fromList [L,R]),("c",fromList [R])]
注意事项:
注意mplus
用于分支一元计算的用法。从MonadPlus
使用mplus
和mzero
(或派生msum
、mfilter
、guard
等)比直接使用列表操作更方便。如果您稍后更改您的 monad 堆栈,例如从 []
更改为我们的 NonDet Direction
,您现有的代码将无需修改即可工作。
对于WriterT
,我们可以使用任何幺半群,而不仅仅是序列。例如,如果我们只关心所采取的步数,我们可以定义
type NonDet a = WriterT (Sum Int) [] a
mark :: NonDet w ()
mark tell (Sum 1)
然后调用mark
只会增加我们的计数器,调用的结果(稍微修改traverse
)将是
[("a",Sum getSum = 2),("b",Sum getSum = 2),("c",Sum getSum = 1)]
【讨论】:
【参考方案2】:使用 list monad 可以让您像树一样构建计算,但它会丢失源信息。最后,您会得到一个结果列表,但您不知道每个结果的来源。
如果这就是你所关心的,那么列表单子就是完美的。假设您有一个不确定的step
函数:
step :: State -> [State]
如果我们只想多步执行,我们可以这样写:
startState >>= step >>= step >>= step
这将在 3 个步骤后为我们提供所有可能的结果。如果我们想将其推广到任何数字,我们可以使用来自Control.Monad
的一元组合运算符(<=<)
编写一个简单的辅助函数。这就像.
一样工作,除了a -> m b
形式的函数而不是普通函数(a -> b
)。它可能看起来像这样:
stepN :: Int -> (State -> [State]) -> State -> [State]
stepN n f = foldr (<=<) return (replicate n f)
现在要获得三个不确定的步骤,我们可以写stepN 3 step
。 (您可能想为这些函数想出更好的名称:P。)
总而言之:使用 list monad,计算本身 的形状像一棵树,但您只能查看最后的结果。这从所涉及的类型中应该很清楚:最后,您会得到一个[State]
,它本质上是扁平的。但是,函数State -> [State]
有分支,因此到达末尾的计算必须看起来像一棵树。
对于这样的事情,列表类型使用起来非常方便。
【讨论】:
是的,我认为stepN :: Int -> (State -> [State]) -> [State]
和stepN
n f = ...`【参考方案3】:
您实际上可以比其他建议的解决方案做得更好。您可以为每个后续分支保留独立的历史记录并实时跟踪执行路径。
以下是使用 pipes-4.0.0
的方法(目前仍在 Github 上):
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.State
import Pipes
import qualified Pipes.Prelude as P
branch :: Int -> StateT [Int] (ListT' IO) [Int]
branch n =
if (n <= 0) then get
else do
path <- lift $ P.each [1, 2]
lift $ lift $ putStrLn $ "Taking path " ++ show path
modify (path:)
branch (n - 1)
pipe :: () -> Producer' [Int] IO ()
pipe () = runRespondT (evalStateT (branch 3) [])
main = runProxy $ (pipe >-> P.print) ()
这是它的输出:
Taking path 1
Taking path 1
Taking path 1
[1,1,1]
Taking path 2
[2,1,1]
Taking path 2
Taking path 1
[1,2,1]
Taking path 2
[2,2,1]
Taking path 2
Taking path 1
Taking path 1
[1,1,2]
Taking path 2
[2,1,2]
Taking path 2
Taking path 1
[1,2,2]
Taking path 2
[2,2,2]
通常,如果您想保存当前访问状态的上下文,您会使用:
StateT [node] [] r
...node
是您去过的地方。 StateT
跟踪您访问的每个节点,[]
是非确定性部分。但是,如果要添加效果,则需要将 []
替换为等效的 monad 转换器:ListT
:
StateT [node] (ListT IO) r
这就是您派生branch
类型的方式。在我们的特殊情况下,我们正在访问的node
s 是Int
s,branch
在它所采用的每条路径的末尾返回当前上下文。
当你 evalStateT
时,你会得到一个空的初始上下文:
evalStateT (branch 3) [] :: ListT IO [Int]
这是一个非确定性的计算,它将尝试每个分支,在 IO
中跟踪结果,然后在结果结束时返回本地上下文。由于我们的branch
总共将采用 8 条路径,因此将有 8 个最终结果。
如果我们使用runRespondT
运行它,我们会得到一个Producer
:
pipe :: () -> Producer' [Int] IO ()
此生产者将在到达每个执行路径的末端时发出结果,并在执行过程中进行跟踪。我们不必等到计算结束才能看到痕迹。我们需要查看它正在输出的[Int]
s,只需将其连接到Consumer
:
P.print :: () -> Consumer [Int] IO r
pipe >-> P.print :: () -> Effect IO ()
这会将我们的最终计算转换为基本 monad 中的 Effect
(在本例中为 IO
)。我们可以使用runProxy
运行这个效果:
runProxy $ (pipe >-> P.print) () :: IO ()
然后,这将跟踪计算并打印出每条路径的终点。
【讨论】:
以上是关于你如何使用 list monad 来计算/表示非确定性计算的结果?的主要内容,如果未能解决你的问题,请参考以下文章