在第181页的 Beginning Haskell 一书中,有一个使用WriterT
包装List
monad的示例。下面的代码计算图表中的路径。请注意,这是一个非常简单的算法,不考虑循环)。
type Vertex = Int
type Edge = (Vertex, Vertex)
pathsWriterT :: [Edge] -> Vertex -> Vertex -> [[Vertex]]
pathsWriterT edges start end = execWriterT (pathsWriterT' edges start end)
pathsWriterT' :: [Edge] -> Vertex -> Vertex -> WriterT [Vertex] [] ()
pathsWriterT' edges start end =
let e_paths = do (e_start, e_end) <- lift edges
guard $ e_start == start
tell [start]
pathsWriterT' edges e_end end
in if start == end
then tell [start] `mplus` e_paths
else e_paths
在let
的{{1}}和in
块中,我告诉编写者将当前顶点添加到路径中。但是后来在pathsWriterT'
通过执行作者我得到了可能的路径列表。
Writer如何将所有计算出的路径组合到路径列表中?如何在pathsWriterT
表示的单个计算中独立“存储”不同的路径? (原谅我的命令性语言)
答案 0 :(得分:9)
请记住,Haskell中的Monad
是m :: * -> *
类型,支持两种操作:
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
虽然考虑do
中的一系列操作 - 作为计算的符号通常很有用,但当您对引擎盖下的内容感兴趣时,您应该考虑一下类型m a
的值以及涉及return
和(>>=)
时会发生什么。
有问题的monad是WriterT [Vertex] []
。这就是WriterT
的定义方式:
newtype WriterT w m a = WriterT { runWriterT :: m (a, w) }
将[Vertex]
替换为w
,将[]
替换为m
。我们得到了这个:
[(a, [Vertex])]
所以它是a
类型的值列表,每个值都有一个与之关联的顶点列表。这些类型等效 modulo newtype wrap / unwrapping。现在,我们需要了解return
和(>>=)
如何适用于此类型。
return
的{p> []
创建了一个单例列表。因此return 'x' :: [Char]
是['x']
。 return
WriterT
将累加器设置为mempty
,并将其余作业委托给内部monad的return
。
在我们的例子中,累加器的类型为[Vertex]
,mempty :: [Vertex]
为[]
。这意味着return 'x' :: WriterT [Vertex] [] Char
表示为[('x', [])]
- 带有空顶点列表的'x'
字符。这非常简单:我们monad的return
方法创建一个单例列表,其中没有顶点与此列表中的唯一值相关联。
当然,棘手的部分是(>>=)
运算符(发音为&#34; bind&#34;,如果你不知道的话)。对于列表,它的类型为[a] -> (a -> [b]) -> [b]
。它的语义是函数a -> [b]
将应用于[a]
中的每个元素,结果[[b]]
将被连接。
[a, b, c] >>= f
将成为f a ++ f b ++ f c
。一个简单的例子来说明:
[10, 20, 30] >>= \a -> [a - 5, a + 5]
你能弄清楚结果列表会是什么吗? (如果没有,请运行GHCi中的示例。)
没有什么可以阻止您在提供给另一个(>>=)
的函数中使用(>>=)
:
[10, 20, 30] >>= \a ->
[subtract 5, (+5)] >>= \f ->
[f a]
实际上,这就是do
- 符号的工作原理。上面的例子相当于:
do
a <- [10, 20, 30]
f <- [subtract 5, (+5)]
return (f a)
因此,它就像构建一个值树,然后将其展平为一个列表。初始树:
a <- (10)-----------------(20)------------------(30)
| | |
| | |
v v v
f <- (subtract 5)----(+5) (subtract 5)----(+5) (subtract 5)----(+5)
| | | | | |
| | | | | |
v v v v v v
[f a] [f a] [f a] [f a] [f a] [f a]
第1步(替换f
):
a <- (10)-----------------(20)-------------------(30)
| | |
| | |
v v v
[subtract 5 a, a + 5] [subtract 5 a, a + 5] [subtract 5 a, a + 5]
第2步(替换a
):
[subtract 5 10, 10 + 5, subtract 5 20, 20 + 5, subtract 5 30, 30 + 5]
然后,当然,它减少到[5, 10, 15, 20, 25, 30, 35]
。
现在,您可以记住,WriterT
为每个值添加累加器。因此,在展平树的每个步骤中,它将使用mappend
来合并这些累加器。
让我们回到你的例子pathWriterT'
。为了便于理解,我将稍微修改一下以删除自循环的处理并使绑定单元显式化:
pathsWriterT' :: [Edge] -> Vertex -> Vertex -> WriterT [Vertex] [] ()
pathsWriterT' edges start end
| start == end = tell [end]
| otherwise = do
(e_start, e_end) <- lift edges
() <- guard $ e_start == start
() <- tell [start]
pathsWriterT' edges e_end end
考虑调用pathsWriterT'
其中
edges
= [(1,2), (2,3), (2,4)]
start
= 1
end
= 4
再次,我们可以绘制一棵树,但它会更加复杂,所以让我们逐行进行:
{- Line 1 -} (e_start, e_end) <- lift edges
{- Line 2 -} () <- guard $ e_start == start
{- Line 3 -} () <- tell [start]
{- Line 4 -} pathsWriterT' edges e_end end
第1行。edges
的类型为[Edge]
。当您从lift
应用MonadTrans
时,它会变为WriterT [Vertex] [] Edge
。请记住,简而言之,这只是[(Edge, [Vertex])]
。 lift
WriterT
的实现非常简单:为每个值设置累加器为mempty
。因此,现在我们lift edges
等于:
[ ((1,2), []) ,
((2,3), []) ,
((2,4), []) ]
我们的树是:
(e_start, e_end) <- ((1,2), [])------((2,3), [])-----((2,4), [])
对于每个(e_start, e_end)
值,会发生以下情况......
第2行。边的源顶点绑定到e_start
,目标顶点绑定到e_end
。 guard
在其参数为return ()
时展开为True
,在其empty
时展开为False
。对于列表,return ()
为[()]
,empty
为[]
。对于我们的monad,我们有相同的但有累加器:return ()
是[((), [])]
而empty
仍然是[]
(因为没有值可以将累加器附加到)。由于我们决定start
= 1
,评估guard
的结果是:
(1,2)
,[((), [])]
(2,3)
,[]
(2,4)
,[]
有三个结果,因为我们正在使用每个元素。让我们将它们添加到我们的树中:
(e_start, e_end) <- ((1,2), [])------((2,3), [])-----((2,4), [])
| | |
| | |
v none none
() <- ((), [])
如您所见,我写了none
代替(2,3)
和(2,4)
的子节点。因为guard
没有为他们提供子节点,所以它返回了一个空列表。现在我们继续......
第3行。现在我们使用tell
来扩展累加器。 tell
返回单位值()
,但附加了累加器。由于start
等于1
,因此累加器将为[1]
。所以让我们调整我们的树:
(e_start, e_end) <- ((1,2), [])------((2,3), [])-----((2,4), [])
| | |
| | |
v none none
() <- ((), [])
|
|
v
() <- ((), [1])
第4行。现在我们调用pathsWriterT' edges e_end end
以递归方式继续构建树!凉。在这个递归调用中:我们有:
edges
=旧edges
start
=旧e_end
= 2
end
=旧end
= 4
我们回到第1行。我们的树现在看起来像这样:
(e_start, e_end) <- ((1,2), [])------((2,3), [])-----((2,4), [])
| | |
| | |
v none none
() <- ((), [])
|
|
v
() <- ((), [1])
|
|\_________________________________
| | |
v v v
(e_start, e_end) <- ((1,2), []) ((2,3), []) ((2,4), [])
再次第2行......只是这一次,它会给我们留下不同的节点(因为start
已经改变了)!
(e_start, e_end) <- ((1,2), [])------((2,3), [])-----((2,4), [])
| | |
| | |
v none none
() <- ((), [])
|
|
v
() <- ((), [1])
|
|\_________________________________
| | |
v v v
(e_start, e_end) <- ((1,2), []) ((2,3), []) ((2,4), [])
| | |
| | |
none v v
() <- ((), []) ((), [])
再次第3行,现在它将[2]
添加为累加器。
(e_start, e_end) <- ((1,2), [])------((2,3), [])-----((2,4), [])
| | |
| | |
v none none
() <- ((), [])
|
|
v
() <- ((), [1])
|
|\_________________________________
| | |
v v v
(e_start, e_end) <- ((1,2), []) ((2,3), []) ((2,4), [])
| | |
| | |
none v v
() <- ((), []) ((), [])
| |
| |
v v
() <- ((), [2]) ((), [2])
在第4行,我们进入pathsWriterT'
。
edges
=旧edges
start
=旧e_end
= 3
,4
end
=旧end
= 4
请注意,我将3
和4
都写为e_end
的值。这是因为递归发生在两个分支中:
(2,3)
中,我们将再次创建每个边缘的孩子。(2,4)
中,请注意start == end
成立,结束递归。我们创建了一个小孩[((), [4])]
,因为这是我们monad的tell [4]
的结果。(e_start, e_end) <- ((1,2), [])------((2,3), [])-----((2,4), [])
| | |
| | |
v none none
() <- ((), [])
|
|
v
() <- ((), [1])
|
|\_________________________________
| | |
v v v
(e_start, e_end) <- ((1,2), []) ((2,3), []) ((2,4), [])
| | |
| | |
none v v
() <- ((), []) ((), [])
| |
| |
v v
() <- ((), [2]) ((), [2])
| |
____________________|____ v
| | | [((), [4])]
v v v
(e_start, e_end) <- ((1,2), []) ((2,3), []) ((2,4), [])
在第2行,警卫不会让任何新的孩子出现在这里,因为没有节点可以满足e_start == 4
。
(e_start, e_end) <- ((1,2), [])------((2,3), [])-----((2,4), [])
| | |
| | |
v none none
() <- ((), [])
|
|
v
() <- ((), [1])
|
|\_________________________________
| | |
v v v
(e_start, e_end) <- ((1,2), []) ((2,3), []) ((2,4), [])
| | |
| | |
none v v
() <- ((), []) ((), [])
| |
| |
v v
() <- ((), [2]) ((), [2])
| |
____________________|____ v
| | | [((), [4])]
v v v
(e_start, e_end) <- ((1,2), []) ((2,3), []) ((2,4), [])
| | |
| | |
none none none
() <-
呼!我们的树是建造的。现在是减少它的时候了。我会在每个缩小步骤中将树的深度减少1,自下而上。在每个缩减步骤中,我将用其子级的连接列表替换父级,并将父级的累加器mappend
替换为其子级的累加器。为什么这个逻辑呢?嗯,这就是为我们的monad定义(>>=)
的方式。
请注意,我们树的叶子的类型为[((), [Vertex])]
- 这是pathsWriterT'
的返回类型。请记住,none
代表空列表[]
,因此它也具有此类型。内部节点的类型为(a, [Vertex])
,其中a
是绑定变量的类型(我在树的左侧绘制了变量绑定)。
第1步。
(e_start, e_end) <- ((1,2), [])------((2,3), [])-----((2,4), [])
| | |
| | |
v none none
() <- ((), [])
|
|
v
() <- ((), [1])
|
|\_________________________________
| | |
v v v
(e_start, e_end) <- ((1,2), []) ((2,3), []) ((2,4), [])
| | |
| | |
none v v
() <- ((), []) ((), [])
| |
| |
v v
() <- ((), [2]) ((), [2])
| |
____________________|____ v
| | | [((), [4])]
none none none
第2步。
(e_start, e_end) <- ((1,2), [])------((2,3), [])-----((2,4), [])
| | |
| | |
v none none
() <- ((), [])
|
|
v
() <- ((), [1])
|
|\_________________________________
| | |
v v v
(e_start, e_end) <- ((1,2), []) ((2,3), []) ((2,4), [])
| | |
| | |
none v v
() <- ((), []) ((), [])
| |
| |
v v
() <- ((), [2]) ((), [2])
| |
none v
[((), [4])]
第3步。
(e_start, e_end) <- ((1,2), [])------((2,3), [])-----((2,4), [])
| | |
| | |
v none none
() <- ((), [])
|
|
v
() <- ((), [1])
|
|\_________________________________
| | |
v v v
(e_start, e_end) <- ((1,2), []) ((2,3), []) ((2,4), [])
| | |
| | |
none v v
() <- ((), []) ((), [])
| |
| |
none v
[((), [2,4])]
第4步。
(e_start, e_end) <- ((1,2), [])------((2,3), [])-----((2,4), [])
| | |
| | |
v none none
() <- ((), [])
|
|
v
() <- ((), [1])
|
|\_________________________________
| | |
v v v
(e_start, e_end) <- ((1,2), []) ((2,3), []) ((2,4), [])
| | |
| | |
none none v
[((), [2,4])]
第5步。
(e_start, e_end) <- ((1,2), [])------((2,3), [])-----((2,4), [])
| | |
| | |
v none none
() <- ((), [])
|
|
v
() <- ((), [1])
|
|\_________________________________
| | |
none none v
[((), [2,4])]
第6步。
(e_start, e_end) <- ((1,2), [])------((2,3), [])-----((2,4), [])
| | |
| | |
v none none
() <- ((), [])
|
|
v
[((), [1,2,4])]
第7步。
(e_start, e_end) <- ((1,2), [])------((2,3), [])-----((2,4), [])
| | |
| | |
v none none
[((), [1,2,4])]
第8步。
[((), [1,2,4])]
execWriterT
会丢弃这些值,只留下累加器,现在我们只剩下[[1,2,4]]
,这意味着1
只有一条路径到4
:[1,2,4]
。
练习:对edges
= [(1,2), (1,3), (2,4), (3,4)]
执行相同操作(使用笔和纸)。你应该得到[[1,2,4], [1,3,4]]
。