如何使用列表monad来计算/表示非确定性计算的结果?

时间:2013-06-18 14:11:12

标签: haskell monads non-deterministic

我想构建一个计算,其中上下文是指向当前(形成树)的所有路径的历史,并且该函数是以过去状态为条件的当前状态。函数本身是非确定性的,因此一个过去的状态可能导致几个未来状态,因此树分支。将这个计算的结果表示为树是有意义的,但有没有办法用列表monad简洁地表达它?或者其他一些我不知道的构造?

3 个答案:

答案 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(或派生msummfilterguard等)比使用列表操作更方便直。如果您以后更改了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类型的方法。在我们的特定情况下,我们访问的nodeInt 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 ()

这样就可以跟踪计算并打印出每条路径的终点。