这里有两个因素在起作用。 <>/mappend调用的嵌套方式,以及整个日志是否保存在内存中。
<> 调用是如何嵌套的?
这取决于您如何使用 Writer 编写代码,而不是取决于 Writer 的实现。要知道为什么,让我们作弊吧。
data Tree a = Nil | Leaf a | Node (Tree a) (Tree a)
deriving (Show)
instance Semigroup (Tree a)
where x <> y = Node x y
instance Monoid (Tree a)
where mempty = Nil
这不是一个适当的幺半群,因为<> 不是关联的。 x <> (y <> z) 给出Node x (Node y z) 而(x <> y) <> z) 给出Node (Node x y) z。它允许我们事后判断 Writer 的“日志”是减少了左嵌套还是右嵌套。
go :: Int -> Writer (Tree Int) ()
go i
| i < 5
= do tell (Leaf i)
go (i+1)
| otherwise
= pure ()
main :: IO ()
main = do
let (result, log) = runWriter $ go 1
putStrLn (render log)
render Nil = "Nil"
render (Leaf x) = show x
render (Node x y) = "(" ++ render x ++ ") <> (" ++ render y ++ ")"
有了这个,你会得到:(1) <> ((2) <> ((3) <> ((4) <> (Nil))))
显然是右嵌套的。因此,您如何生成一个无限列表作为 Writer 的“日志”,并在它是在相对较小的空间中生成时使用它。
但是交换tell 和递归的顺序,使它看起来像这样:
go :: Int -> Writer (Tree Int) ()
go i
| i < 5
= do go (i+1)
tell (Leaf i)
| otherwise
= pure ()
你会得到这个:((((Nil) <> (4)) <> (3)) <> (2)) <> (1)。现在它是左嵌套的,无限递归不起作用:
import Control.Monad.Writer
createInfiniteList :: Int -> Writer [Int] ()
createInfiniteList i = do
createInfiniteList (i+1)
tell [i]
main :: IO ()
main = do
let x = execWriter $ createInfiniteList 1
print x
这永远不会打印任何内容,并且会消耗越来越多的内存。
基本上<> 调用的结构类似于Writer 表达式的结构。在调用绑定到另一个函数的任何地方(包括 do 块中的等效函数),由该调用产生的所有 <> 调用都将位于“括号内”。所以tell _ >> recurse 导致右嵌套<>s,而recurse >> tell _ 导致左嵌套<>s,更复杂的调用图导致<>s 的类似结构嵌套。
强制结果构建整个日志
您的测试程序的另一个特别之处是它根本不使用 Writer 的“结果”,而只使用“日志”。显然,如果递归是无限的,则根本不会有任何最终结果,但是如果我们像这样更改您的程序:
import Control.Monad.Writer
createLargeList :: Int -> Writer [Int] ()
createLargeList i
| i < 50000000
= do tell [i]
createLargeList (i+1)
| otherwise
= pure ()
main :: IO ()
main = do
let (result, log) = runWriter $ createLargeList 1
print $ length log
print result
然后它的行为类似; length 在列表生成时使用它,并在短时间内完成(并且内存使用量相对较低)。之后,() 随时可用并立即打印出来。
但是如果我们把它改成先打印结果:
import Control.Monad.Writer
createLargeList :: Int -> Writer [Int] ()
createLargeList i
| i < 50000000
= do tell [i]
createLargeList (i+1)
| otherwise
= pure ()
main :: IO ()
main = do
let (result, log) = runWriter $ createLargeList 1
print result
print $ length log
然后在我的系统上,这需要更长的时间,并且消耗近 15 GB 的 RAM1。它确实必须在 RAM 中完全实现日志才能获得最终结果,即使 <>s 是右嵌套的并且可以延迟使用日志。
从技术上讲,我认为它不是在内存中构建 list,而是将<> 应用于单例列表的一系列 thunk,它与最终列表一样长并且可能使用更多内存链中的每个环节。结果列表仍然由length 使用,因为它是通过强制这些 thunk 生成的,但这并没有真正帮助,因为必须生成整个 thunk 链才能获得最终的 () 结果,而不是拥有 thunk链本身生成为length 需要更多列表。
1 编译就像ghc foo.hs;如果我用-O2 编译,那么它的行为类似于首先打印日志的长度。这是 GHC 内联所有内容并找出计算相同结果的更好方法的一个相当简单的案例;如果程序更复杂,我不会认为它的优化可以解决这个问题。