假设我有如下内容
class C a where
f :: a -> Text
newtype T a = T a
expensiveFunction :: (Bounded a, Enum a, ToText a) => Map a Text
expensiveFunction = _
cheapFunction :: (Bounded a, Enum a, ToText a) => T a -> Map a Text -> Text
cheapFunction = _
instance (Bounded a, Enum a, ToText a) => C (T a) where
f x = cheapFunction x expensiveFunction
我想做的是:
newtype U = U Int
deriving C via (T Int)
我看到的问题是。expensiveFunction
将在每次调用f
时被调用。这很糟糕
我确实有这样的想法,将T a
示例更改为:
instance (Bounded a, Enum a, ToText a) => C (T a) where
f = flip cheapFunction expensiveFunction
然后希望f
(即,一个参数的函数)计算一次,这意味着expensiveFunction
只计算一次。
但我不确定这有多可靠。我猜,如果请求的示例是多态的,那么这可能不起作用,但是如果示例是单态的(即,T Int
以上),然后我希望这将做的伎俩。
但是f
不是一个普通的函数,它是一个示例函数,并且由于f
在类C
中被定义为函数,而不是一个独立的值,所以它可能会被当作一个函数对待,每次都要重新计算。
有什么想法吗?有没有更好的方法我应该在这里使用。如果可能的话,我想保留通过语法派生的好方法。
1条答案
按热度按时间3pvhb19x1#
我做了一些快速的实验。您建议使用
flip
将expensiveFunction
移动到f
的最终应用程序之外,这似乎非常有效。下面是我用的:
如果没有优化,我会得到这个:
因此,正如您所想的那样,像
T
这样的多态示例每次都会重新计算expensiveFunction
(因为f
是Bounded a
等字典的函数,而expensiveFunction
的计算发生在f
应用于示例字典的内部,即使它在f
应用于其最终参数的外部)。但是
U
的f
能够保留单个共享expensiveFunction
,因此即使没有任何优化,它也只评估一次。如
g
所示,您始终可以定义单态变体,这将确保expensiveFunction
在单态变体的所有使用中共享。当编译与优化时,我得到了这个:
编译器已经注意到所有这些最终都使用了
expensiveFunction @Int
,并且甚至在不同newtype的示例之间共享它。通过优化,即使没有应用
flip
优化,我也会得到这种行为。就我个人而言,我愿意在未优化的版本中依靠
expensiveFunction
在f @U
调用(和g
调用)中的共享。它符合我对一个相当天真的心理模型的期望,在这个模型中,共享是由let绑定和作用域决定的,所以我认为来自改变实现或未来编译器版本的扰动不太可能比这更糟糕。因此,如果共享像C U
这样的单态示例和像g
这样的单态 Package 器就足够了,那么我只需要应用你的flip
优化就可以了。我会有点不太舒服依赖于精确的优化行为;它们可能会受到编译器决定内联多少的影响,这在未来的编译器版本中很容易改变,或者当我对代码进行更改时。因此,如果在所有示例之间共享(甚至对于
C (T a)
这样的多态示例)是至关重要的,那么我想考虑编写一些不那么“漂亮”的代码,使共享更显式。但是,如果这种额外的共享只是一个很好的性能提升,而不是关键任务,那么就没有必要太担心。