你如何使用 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使用mplusmzero(或派生msummfilterguard等)比直接使用列表操作更方便。如果您稍后更改您的 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 -&gt; [State] 有分支,因此到达末尾的计算必须看起来像一棵树。

对于这样的事情,列表类型使用起来非常方便。

【讨论】:

是的,我认为stepN :: Int -&gt; (State -&gt; [State]) -&gt; [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 类型的方式。在我们的特殊情况下,我们正在访问的nodes 是Ints,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 来计算/表示非确定性计算的结果?的主要内容,如果未能解决你的问题,请参考以下文章

Scala中的Monad特征

如何使用 Monad.Writer 进行跟踪?

J 中的术语“monadic”是不是与它的 Haskell 使用有关?

Monads - 定义,法律和例子

haskell列表理解通过列表monad理解

Monad的重点