我的应用程序在使用FFT进行(昂贵)转换后将向量相乘。结果,当我写
f :: (Num a) => a -> [a] -> [a]
f c xs = map (c*) xs
我只想计算一次c
的FFT,而不是xs
的每个元素。确实没有必要为整个程序存储c
的FFT,只需在本地范围内。
我尝试定义我的Num
实例,如:
data Foo = Scalar c
| Vec Bool v -- the bool indicates which domain v is in
instance Num Foo where
(*) (Scalar c) = \x -> case x of
Scalar d -> Scalar (c*d)
Vec b v-> Vec b $ map (c*) v
(*) v1 = let Vec True v = fft v1
in \x -> case x of
Scalar d -> Vec True $ map (c*) v
v2 -> Vec True $ zipWith (*) v (fft v2)
然后,在一个应用程序中,我调用一个类似于f
的函数(适用于任意Num
s)c=Vec False v
,我希望这个函数和如果我将f
改为:
g :: Foo -> [Foo] -> [Foo]
g c xs = let c' = fft c
in map (c'*) xs
函数g
使fft c
的记忆失效,并且比调用f
要快得多(无论我如何定义(*)
)。我不明白f
出了什么问题。是我在(*)
实例中对Num
的定义吗?是否与f
在所有Nums上工作有关,因此GHC无法弄清楚如何部分计算(*)
?
注意:我检查了我的Num实例的核心输出,并且(*)确实表示为嵌套的lambdas,顶层lambda中的FFT转换。所以看起来这至少能够被记忆。我也尝试过使用爆炸模式进行明智和鲁莽的使用,试图强制评估无效。
作为旁注,即使我可以弄清楚如何使(*)
记住它的第一个参数,还有另一个问题如何定义:程序员想要使用 Foo数据类型必须知道这个memoization功能。如果她写了
map (*c) xs
不会发生任何记忆。 (它必须写成(map (c*) xs))
现在,我想起来,我不完全确定GHC会如何重写(*c)
版本,因为我已经(*)
。但我做得很快测试以验证(*c)
和(c*)
是否按预期工作:(c*)
使c
成为*
的第一个参数,而(*c)
使c
成为*
1}}第二个arg到*
。所以问题是人们应该如何编写乘法来确保记忆是不明显的。这只是中缀符号的固有缺点(以及隐含的假设,即参数到{{1}}是对称的吗?
第二个不太紧迫的问题是我们将(v *)映射到标量列表的情况。在这种情况下,(希望)v的fft将被计算和存储,即使它是不必要的,因为另一个被乘数是标量。有没有办法解决这个问题?
由于
答案 0 :(得分:2)
我相信stable-memo包可以解决您的问题。它不会使用相等而是通过引用标识来记忆值:
尽管大多数备忘录组合器基于相等性进行memoize,但是stable-memo基于之前是否已经将完全相同的参数传递给函数(即,在内存中是相同的参数)来实现。
当它们的密钥被垃圾收集时,它会自动删除记忆值:
stable-memo不会保留到目前为止看到的密钥,如果不再使用它们,它们将被垃圾收集。如果发生这种情况,将使用终结器来从备忘录表中删除相应的条目。
因此,如果您定义类似
的内容fft = memo fft'
where fft' = ... -- your old definition
你会得到你需要的东西:调用map (c *) xs
会在第一次调用(*)
时记住fft的计算,然后在(c *)
的后续调用中重用它。如果c
被垃圾收集,那么fft' c
也是如此。
另请参阅this answer至How to add fields that only cache something to ADT?
答案 1 :(得分:1)
我可以看到两个可能阻止记忆的问题:
首先,f
具有重载类型,适用于所有Num
个实例。所以f
不能使用memoization,除非它是专门的(通常需要SPECIALIZE
pragma)或内联(可能会自动发生,但使用INLINE
pragma更可靠)。
其次,(*)
的{{1}}定义对第一个参数执行模式匹配,但Foo
与未知f
相乘。所以在c
内,即使是专门的,也不会发生任何记忆。再次,它在很大程度上取决于f
被内联,并且提供了f
的具体参数,以便实际上可以出现内联。
所以我认为看看你究竟打电话给c
是多么有帮助。请注意,如果使用两个参数定义f
,则必须为其提供两个参数,否则无法内联。此外,有助于查看f
的实际定义,因为您提及的Foo
和c
不在范围内。