在列表(Writer [w] a
)上实施Haskell writer monad将使用++
添加项目。因此,如果我在列表编写器monad中编写此代码:
do
tell [a, b, c]
tell [d]
列表将附加[a, b, c] ++ [d]
。在OCaml中工作之后,我内部化了应该使用cons运算符(:
而不是串联运算符(++
)来构建列表,因为后者的第一个参数是O(n)。
我的工作量一次向作家单声道添加一个“消息”,因此++
的第二个参数通常是一个单例列表。
在Haskell中,懒惰会使列表编写者monad变得比OCaml这样的急切语言更有效吗?如果不是这样,那么对我的工作量有什么有效的替代方法?
答案 0 :(得分:5)
与左相关的h = 0
效率低下,因为最左边的列表被遍历了多次,每个包围(++)
一次。右相关的(++)
很好(至少,不能直接使用(++)
来提高效率)。
标准(:)
转换器(和WriterT
编写者)以与绑定关联相同的方式将其调用与(,)
关联。因此,通过扩展前面的讨论,与左相关的(++)
会出现问题,而与右相关的(>>=)
会很好。特别是,这意味着会产生抽象费用。如果在重构中要拉出下面do块的前两行:
x = do
tell a
tell b
tell c
进入一个单独的定义,也许是因为它们经常发生:
y = do
tell a
tell b
x = do
y
tell c
此重构将左侧的一个绑定重新关联,因此成本略高。
万一这让您担心,可以通过使用标准差异列表技巧作为半身像来选择稍有不同的权衡。所以:
do
tell (Endo ([a,b,c]++))
tell (Endo ([d]++))
这将神奇地将您的(++)
关联到右侧(哇!每次我重新确定其工作原理时,我的脑海都震撼了)。代价是差异列表的每个观察(即从差异列表到标准列表的转换)都很昂贵(而以前选择裸列表的情况下,多个观察的成本不超过一个观察)。如果您只有一个消费者(例如,一次对runWriterT
的顶级调用,一次又一次地将列表弄平了),那么这就渐近而言没问题,但是如果您发现自己正在调用listen
或{ {1}}并经常检查差异列表,您可能不想选择它。
如果这些折衷选择都不对您有利,那么第三种选择是使用手指树,例如pass
的观察是免费的(与差异列表不同),并且两端的连接在较短的自变量中是对数时间(与标准列表不同,在第一个自变量中它是线性的),但是常数是足够高,您可以在很多情况下注意到它。