我想构建一个计算,其中上下文是指向当前(形成树)的所有路径的历史,并且该函数是以过去状态为条件的当前状态。函数本身是非确定性的,因此一个过去的状态可能导致几个未来状态,因此树分支。将这个计算的结果表示为树是有意义的,但有没有办法用列表monad简洁地表达它?或者其他一些我不知道的构造?
答案 0 :(得分:6)
使用list monad可以让你像树一样构建计算,但它会丢失源信息。最后,您将得到一个结果列表,但您不知道每个结果的来源。
如果您只关心这一点,那么列表monad是完美的。我们假设你有一个非确定性的step
函数:
step :: State -> [State]
如果我们想要多次执行它,我们可以编写如下内容:
startState >>= step >>= step >>= step
这将通过3个步骤为我们提供所有可能的结果。如果我们想将它推广到任何数字,我们可以使用(<=<)
中的monadic组合运算符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]
分支,所以到达终点的计算必须看起来像一棵树。
对于类似的东西,列表类型使用起来非常方便。
答案 1 :(得分:6)
我想补充一下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
用于分支monadic计算的用法。使用来自mplus
的{{1}}和mzero
(或派生msum
,mfilter
,guard
等)比使用列表操作更方便直。如果您以后更改了monad堆栈,例如从MonadPlus
更改为[]
,则现有代码无需修改即可使用。对于NonDet Direction
,我们可以使用任何monoid,而不仅仅是序列。例如,如果我们关心的是所采取的步骤数,我们可以定义
WriterT
然后调用type NonDet a = WriterT (Sum Int) [] a
mark :: NonDet w ()
mark tell (Sum 1)
只会增加我们的计数器,调用的结果(略微修改mark
)将是
traverse
答案 2 :(得分:2)
实际上,您可以比其他提议的解决方案做得更好。您可以为每个连续分支保留独立历史记录,并实时跟踪执行路径。
以下是使用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
是Int
s,branch
返回每个路径末尾的当前上下文。
当evalStateT
具有空的初始上下文时,您会得到:
evalStateT (branch 3) [] :: ListT IO [Int]
这是一个非确定性计算,它会尝试每个分支,在IO
跟踪结果时跟踪结果,然后在结果的末尾返回本地上下文。由于我们的branch
将占用总共8条路径,因此将有8个最终结果。
如果我们使用runRespondT
运行,我们会得到Producer
:
pipe :: () -> Producer' [Int] IO ()
此生产者将在到达每个执行路径的末尾时发出结果,并在其进行跟踪时进行跟踪。我们不必等到计算结束才能看到跟踪。我们需要查看它输出的[Int]
是将它连接到Consumer
:
P.print :: () -> Consumer [Int] IO r
pipe >-> P.print :: () -> Effect IO ()
这会将我们的最终计算转换为基础monad中的Effect
(在本例中为IO
)。我们可以使用runProxy
:
runProxy $ (pipe >-> P.print) () :: IO ()
这样就可以跟踪计算并打印出每条路径的终点。