You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在Haskell类实例中缓存高成本计算结果的实现疑问

问题:避免expensiveFunction在类实例中重复计算

原始代码如下:

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

我希望通过deriving via语法实现U的C实例:

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

但发现每次调用f时,expensiveFunction都会被重新计算,导致性能问题。

我尝试修改T a的C实例定义为:

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

期望f作为单参函数只被计算一次,从而让expensiveFunction仅执行一次。但不确定这种方式是否可靠:猜测多态实例下可能无效,但单态实例(比如T Int)或许可行。不过f是类中的实例函数而非独立值,可能每次调用仍会重新计算。

请问这种写法是否可靠?有没有更好的实现方法?希望尽量保留简洁的deriving via语法。


你的写法是否可靠?

对于单态实例(比如T Int),这种写法是可靠的。GHC这类Haskell编译器会对单态值做共享优化——当你把f定义为flip cheapFunction expensiveFunction时,针对具体的a类型(比如Int),expensiveFunction @Int会被计算一次并缓存,之后每次调用f都会复用这个缓存好的Map。

但对于多态的T a,情况不同:因为expensiveFunction是多态的,编译器无法在实例层面缓存它的值,每次调用f @(T a)(如果a仍是多态的)时,expensiveFunction可能会被重新计算。不过你的场景中使用的是单态的T Int,所以这个问题不会出现。

更好的实现方法

如果想确保无论单态还是多态场景下,expensiveFunction都只计算一次,同时保留deriving via语法,可以参考以下几种方式:

1. 全局单态值缓存

为每个需要的类型定义全局的单态expensiveFunction,然后在对应T a的实例中使用它:

-- 全局单态值,程序启动时仅计算一次
expensiveFunctionInt :: Map Int Text
expensiveFunctionInt = expensiveFunction @Int

instance C (T Int) where
  f = flip cheapFunction expensiveFunctionInt

此时U的deriving via可以直接复用T Int,因为T Int的f会复用全局缓存好的expensiveFunctionInt。

2. 编译期预计算(进阶)

如果expensiveFunction的结果可以在编译期确定(比如基于Bounded和Enum的枚举值生成Map),可以用TemplateHaskell提前生成对应类型的Map值,彻底避免运行时计算:

import Language.Haskell.TH

-- 编译期生成指定类型的expensiveFunction
genExpensiveFunction :: Name -> Q Exp
genExpensiveFunction tyName = do
  -- 这里实现编译期遍历枚举值、生成Map的逻辑
  -- 示例逻辑,实际需根据ToText实现调整
  [t| Map $(conT tyName) Text |] >>= \ty -> do
    let vals = [minBound..maxBound] :: [Int] -- 替换为动态获取的类型枚举值
    mapExp <- listToMap vals -- 自定义函数,将枚举值列表转为Map的TH表达式
    return mapExp

-- 为Int生成编译期计算好的Map
expensiveFunctionInt :: Map Int Text
expensiveFunctionInt = $(genExpensiveFunction ''Int)

instance C (T Int) where
  f = flip cheapFunction expensiveFunctionInt

这种方式同样可以配合deriving via使用,无需修改U的定义。

3. 关联值缓存(需修改类结构)

如果允许修改原有的C类,可以添加关联值存储缓存的Map,确保每个实例只计算一次:

-- 定义类型族,获取T a的底层类型
type family UnderlyingType a where
  UnderlyingType (T a) = a

class C a where
  f :: a -> Text
  cachedMap :: Map (UnderlyingType a) Text

instance (Bounded a, Enum a, ToText a) => C (T a) where
  cachedMap = expensiveFunction @a
  f (T x) = cheapFunction (T x) cachedMap

这种方式能统一处理单态和多态场景,但需要修改原有类的结构,适合可以调整类定义的场景。


总结

  • 你当前的写法对于单态实例(如T Int)完全可靠,GHC会自动缓存expensiveFunction @Int的结果。
  • 如果需要更通用的缓存方案,全局单态值的方式最简洁,既能保留deriving via的语法优势,又能确保expensiveFunction仅计算一次。

内容的提问来源于stack exchange,提问作者Clinton

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 14:05:21