haskell 在类示例中缓存计算开销大的结果

wydwbb8l  于 9个月前  发布在  其他
关注(0)|答案(1)|浏览(66)

假设我有如下内容

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中被定义为函数,而不是一个独立的值,所以它可能会被当作一个函数对待,每次都要重新计算。
有什么想法吗?有没有更好的方法我应该在这里使用。如果可能的话,我想保留通过语法派生的好方法。

3pvhb19x

3pvhb19x1#

我做了一些快速的实验。您建议使用flipexpensiveFunction移动到f的最终应用程序之外,这似乎非常有效。
下面是我用的:

module Main where

import C
import U

main :: IO ()
main = do
  putStrLn "f @(T Int)"
  let a = f (T @Int 1)
      b = f (T @Int 2)
      c = f (T @Int 3)
  a `seq` b `seq` c `seq` print [a, b, c]

  putStrLn "\n\nU"
  let x = f (U 4)
      y = f (U 5)
      z = f (U 6)
  x `seq` y `seq` z `seq` print [x, y, z]

  putStrLn "\n\ng @(T Int)"
  let g = f @(T Int)
      i = g (T 7)
      j = g (T 8)
      k = g (T 9)
  i `seq` j `seq` k `seq` print [i, j, k]
module C
  ( C (..)
  , T (..)
  , ToText (..)
  , expensiveFunction
  , cheapFunction
  )
where

import Data.Map (Map)
import Data.Text (Text)
import Data.Text qualified as Text

import Debug.Trace (trace)

class ToText a where
  toText :: a -> Text
  default toText :: Show a => a -> Text
  toText = Text.pack . show

instance ToText Int

class C a where
  f :: a -> Text

newtype T a = T a

expensiveFunction :: (Bounded a, Enum a, ToText a, Ord a) => Map a Text
expensiveFunction = trace "expensive" $ mempty

cheapFunction :: (Bounded a, Enum a, ToText a) => T a -> Map a Text -> Text
cheapFunction (T x) !_ = trace "cheap" $ toText x

instance (Bounded a, Enum a, ToText a, Ord a) => C (T a) where
  f = flip cheapFunction expensiveFunction
module U
  ( U (..)
  )
where

import C

newtype U = U Int
  deriving C via (T Int)

如果没有优化,我会得到这个:

f @(T Int)
expensive
cheap
expensive
cheap
expensive
cheap
["1","2","3"]

f @U
expensive
cheap
cheap
cheap
["4","5","6"]

g
expensive
cheap
cheap
cheap
["7","8","9"]

因此,正如您所想的那样,像T这样的多态示例每次都会重新计算expensiveFunction(因为fBounded a等字典的函数,而expensiveFunction的计算发生在f应用于示例字典的内部,即使它在f应用于其最终参数的外部)。
但是Uf能够保留单个共享expensiveFunction,因此即使没有任何优化,它也只评估一次。
g所示,您始终可以定义单态变体,这将确保expensiveFunction在单态变体的所有使用中共享。
当编译优化时,我得到了这个:

f @(T Int)
expensive
cheap
cheap
cheap
["1","2","3"]

f @U
cheap
cheap
cheap
["4","5","6"]

g
cheap
cheap
cheap
["7","8","9"]

编译器已经注意到所有这些最终都使用了expensiveFunction @Int,并且甚至在不同newtype的示例之间共享它。
通过优化,即使没有应用flip优化,我也会得到这种行为。
就我个人而言,我愿意在未优化的版本中依靠expensiveFunctionf @U调用(和g调用)中的共享。它符合我对一个相当天真的心理模型的期望,在这个模型中,共享是由let绑定和作用域决定的,所以我认为来自改变实现或未来编译器版本的扰动不太可能比这更糟糕。因此,如果共享像C U这样的单态示例和像g这样的单态 Package 器就足够了,那么我只需要应用你的flip优化就可以了。
我会有点不太舒服依赖于精确的优化行为;它们可能会受到编译器决定内联多少的影响,这在未来的编译器版本中很容易改变,或者当我对代码进行更改时。因此,如果在所有示例之间共享(甚至对于C (T a)这样的多态示例)是至关重要的,那么我想考虑编写一些不那么“漂亮”的代码,使共享更显式。但是,如果这种额外的共享只是一个很好的性能提升,而不是关键任务,那么就没有必要太担心。

相关问题