关于性能:”共享”或”缓存”仅由模糊类型参数化的表达式?

'Share' or 'cache' an expression parameterized by only ambiguous types?

我有一个棘手的问题;

因此,我知道GHC将"缓存"(由于缺乏更好的术语)顶级定义,并且只计算一次,例如:

1
2
myList :: [Int]
myList = fmap (*10) [0..10]

即使我在多个位置使用myList,GHC也会注意到该值没有参数,因此它可以共享它,而不会"重建"列表。

我想这样做,但是要根据类型级别的上下文进行计算;一个简化的示例是:

1
2
dependentList :: forall n. (KnownNat n) => [Nat]
dependentList = [0..natVal (Proxy @n)]

所以这里有趣的是,dependentList没有一个"单个"可缓存值;但是一旦应用了类型,它就会减小到一个常数,因此从理论上讲,一旦运行类型检查器,GHC就会认识到几个点都取决于"相同" dependentList;例如(使用TypeApplications)

1
2
3
4
main = do
  print (dependentList @5)
  print (dependentList @10)
  print (dependentList @5)

我的问题是,GHC会认识到它可以共享两个5列表吗?还是单独计算每个?从技术上讲,甚至有可能在编译时而不是运行时计算这些值,是否有可能让GHC做到这一点?

我的情况稍微复杂一点,但是应该遵循与示例相同的约束条件,但是我的dependentList值很需要计算。

如果有可能,我完全不反对使用typeclass进行操作; GHC会缓存并重用typeclass词典吗?也许我可以将其烘烤为typeclass dict中的常量以进行缓存?

想法有人吗?还是有人在阅读我的书以了解其工作原理?

我希望这样做的方式是使编译器可以解决该问题,而不是使用手动记忆,但是我愿意接受想法:)

感谢您的时间!


按照@crockeea的建议,我进行了一项实验;这是尝试使用具有多态歧义类型变量的顶级常量,也是一个出于娱乐目的的实际常量,每个常量都包含一个\\'trace \\'

1
2
3
4
5
6
7
8
9
10
11
12
13
dependant :: forall n . KnownNat n => Natural
dependant = trace ("eval:" ++ show (natVal (Proxy @n))) (natVal (Proxy @n))

constantVal :: Natural
constantVal = trace"constant val: 1" 1


main :: IO ()
main = do
  print (dependant @1)
  print (dependant @1)
  print constantVal
  print constantVal

结果很不幸:

1
2
3
4
5
6
7
8
λ> main
eval: 1
1
eval: 1
1
constant val: 1
1
1

因此很明显,它每次使用时都会重新评估多态常数。

但是,如果我们将常量写入类型类(仍然使用歧义类型),则似乎每个实例只能解析一次Dictionary值,当您知道GHC对相同的类实例传递相同的dict时,这是有道理的。当然,它确实为不同的实例重新运行了代码:

1
2
3
4
5
6
7
8
9
10
11
class DependantClass n where
  classNat :: Natural

instance (KnownNat n) => DependantClass (n :: Nat) where
  classNat = trace ("dependant class:" ++ show (natVal (Proxy @n))) (natVal (Proxy @n))

main :: IO ()
main = do
  print (classNat @1)
  print (classNat @1)
  print (classNat @2)

结果:

1
2
3
4
5
6
λ> main
dependant class: 1
1
1
dependant class: 2
2

就让GHC在编译时执行这些操作而言,您似乎可以使用此技术使用TemplateHaskell的lift进行操作。

不幸的是,您不能在typeclass定义中使用它,因为TH会抱怨\\'@ n \\'必须从另一个模块(是TH)中导入,并且在编译时具体未知。您可以在任何使用typeclass值的地方执行此操作,但是它将每次提升一次对其进行求值,因此您必须在任何地方提升它,以获取收益。非常不切实际。