Haskell 列表的编写器monad的效率如何?

Haskell 列表的编写器monad的效率如何?,haskell,linked-list,monads,writer,Haskell,Linked List,Monads,Writer,Haskell writer monad on List(writer[w]a)的实现将使用++添加项。因此,如果我在列表编写器monad中编写此代码: do tell [a, b, c] tell [d] 列表将附加[a、b、c]+[d]。在使用OCaml之后,我已经内化了列表应该使用cons操作符(:)而不是串联操作符(++)来构建,因为后者在其第一个参数中是O(n) 我的工作负载一次向writer monad添加一条“消息”,因此++的第二个参数通常是一个单例列表 在Haskel

Haskell writer monad on List(
writer[w]a
)的实现将使用
++
添加项。因此,如果我在列表编写器monad中编写此代码:

do
  tell [a, b, c]
  tell [d]
列表将附加
[a、b、c]+[d]
。在使用OCaml之后,我已经内化了列表应该使用cons操作符(
)而不是串联操作符(
++
)来构建,因为后者在其第一个参数中是O(n)

我的工作负载一次向writer monad添加一条“消息”,因此
++
的第二个参数通常是一个单例列表

在Haskell中,懒惰会使列表编写器monad比在像OCaml这样热切的语言中更高效吗?如果不是的话,对于我的工作负载来说,什么是有效的替代方案?

左关联的
(++)
是低效的,因为最左边的列表被遍历多次,每个封闭的
(++)
一次。右关联的
(++)
很好(至少,直接使用
(:)
无法提高效率)

标准的
WriterT
transformer(和
(,)
writer)将它们的调用与
(++)
相关联,其关联方式与它们的绑定相同。因此,通过扩展前面的讨论,左关联的
(>>=)
s将有问题,而右关联的则可以。特别是,这意味着存在抽象成本。如果在重构中,要拉出下面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
pass
并经常检查差异列表,您可能不想选择此选项


如果您觉得这两种权衡都不合适,那么第三种选择是使用手指树,例如,
Seq
,对于手指树,观察是免费的(不同于差异列表),并且两端的串联是较短参数中的日志时间(不同于标准列表,在第一个参数中是线性的),但常数足够高,在许多情况下你都能注意到。

快速举出一个例子并进行测试。另请参见: