【问题标题】:Is there an automatic way to memoise global polymorphic values in Haskell?在 Haskell 中是否有一种自动记忆全局多态值的方法?
【发布时间】:2014-09-23 08:00:03
【问题描述】:

5 :: Num a => a 这样的多态“常量”并不是真正的常量,而是字典参数的函数。因此,如果你定义

primes :: Num n => [n]
primes = ...

当然是不好的例子,这里没有充分的理由让它多态......我真正感兴趣的是,如果你尝试全局记忆一个非平凡的多态函数,例如memo-tries.
那么这个序列将不会在来自不同站点的调用之间共享,这在性能方面并不好。 (这难道不是 Haskell 标准赋予我们可怕的单态性限制的主要原因吗?)

我能看到如何强制共享的唯一方法是为约束类的每个实例设置一个单态“标签”。例如

erastothenes :: Num n => [n]
erastothenes = ...

class (Num n) => HasPrimes n where
  -- | @'primes' ≡ 'erastothenes'@
  primes :: [n]

integerPrimes :: [Integer]
integerPrimes = erastothenes

instance HasPrimes Integer where
  primes = integerPrimes

...这在优雅方面并不好。

有没有更好的方法来实现这样的记忆?

【问题讨论】:

  • 也许SPECIALIZE 编译指示有用吗?不过,您仍然需要明确列出您想要专门针对的类型。
  • @bennofs:如果SPECIALIZE 可以为此目的可靠地工作,那么这对我来说实际上似乎是最优雅的方式。如果你做一个示例实现,我会接受这个答案。

标签: haskell memoization parametric-polymorphism


【解决方案1】:

出于相当技术原因,这是相当不可能的。类型类是开放的,因此多态常量在编译时不一定“看到”有多少类型满足约束,因此它不能分配那么多单态 thunk。另一方面,一个类型类当然看不到它可能生成的所有可能的常量,所以单态 thunk 不能在类型类字典中分配。

您必须明确提及要分配单态 thunk 的任何类型。

【讨论】:

  • 确实,递归可以产生无限多的实例。 instance Num a => Num (Identity a) where ....
【解决方案2】:

可以将Typeable 约束添加到n 并为每种地面类型n 使用不同的记忆表。为此,您可能需要大量利用Dynamiccast,这是次优的。它也感觉有点hackish。

当然,在依赖类型语言中,可以对映射(n : Num) -> [n] 建模,这不需要来自Dynamiccastss。也许可以利用 GADT 和某种具体化机制来模拟类似的事情。

【讨论】:

    【解决方案3】:

    如果您启用ConstraintKindsExistentialQuantification(或GADTs),您可以具体化类型类字典:

    {-# LANGUAGE ConstraintKinds, ExistentialQuantification #-}
    
    data Dict a = a => Dict
    

    如果我们尝试一下

    fibs :: Num n => [n]
    fibs = 1 : 1 : zipWith (+) fibs (drop 1 fibs)
    
    fibs' :: [Integer]
    fibs' = fibs
    
    
    fibsWithDict :: Dict (Num n) -> [n]
    fibsWithDict Dict = fs
      where
        fs = 1 : 1 : zipWith (+) fs (drop 1 fs)
    
    fibs'' :: [Integer]
    fibs'' = fibsWithDict Dict
    

    在 GHCi 中我们看到

    λ> :set +s
    λ> 
    λ> fibs !! 29
    832040
    (2.66 secs, 721235304 bytes)
    λ> 
    λ> fibs !! 29
    832040
    (2.52 secs, 714054736 bytes)
    λ> 
    λ> 
    λ> fibs' !! 29
    832040
    (2.67 secs, 713510568 bytes)
    λ> 
    λ> fibs' !! 29
    832040
    (0.00 secs, 1034296 bytes)
    λ> 
    λ> 
    λ> fibs'' !! 29
    832040
    (0.00 secs, 1032624 bytes)
    

    所以fibs'' 是三个立即记忆的唯一实现。

    请注意,我们必须在 Dict 构造函数上进行模式匹配。否则,我们将收到关于 n 没有被限制为具有 Num 实例的错误(如果我们的签名只是 fibsWithDict :: a -> [n],就像您所期望的那样)。

    这是一个完整的解决方案,因为您可以将fibsWithDict Dict 视为一个表达式,它可以在您向其输入的任何类型中立即记忆(只要它是Num 的实例)。例如:

    λ> (fibsWithDict Dict !! 29) :: Double
    832040.0
    (0.00 secs, 1028384 bytes)
    

    编辑:看起来这里不需要显式的字典传递,可以通过使用带有本地绑定的ScopedTypeVariables 隐式完成:

    {-# LANGUAGE ScopedTypeVariables #-}
    
    fibsImplicitDict :: forall a. Num a => [a]
    fibsImplicitDict
      = let fs :: [a]
            fs = 1 : 1 : zipWith (+) fs (drop 1 fs)
        in
        fs
    

    (感谢 bennofs 提供的见解!)

    【讨论】:

    • fibs'' 是如何多态的?该问题特别提到了全局 polymorphic 值。接受过的最糟糕的答案。 :P
    • @BoydStephenSmithJr。你说得对,fibs'' 不是 多态的。另一方面,fibsWithDict 多态的,没有通常与类型类多态相关的运行时成本。
    • @BoydStephenSmithJr。我的答案中的最后一个示例(在新的 GHCi 会话中运行)证明了这一点。您可以将 fibsWithDict 约束为任何类型的 Num 实例,而不会产生任何开销。将此行为与fibs 的行为进行对比,由于内部字典传递阻止记忆,其速度非常明显。通过使字典显式传递并在代码中的关键点传递,我们可以避免这种成本。
    • @BoydStephenSmithJr.:我接受了这个答案,因为它提供了一个 slightly 更好的版本来实现我已经在问题中概述的“单态标签”解决方案,并使这个更好的是一个可能的答案。我对此不太满意,但它比“不可能”或“抛出Typeable 和300 行额外代码”要好。跨度>
    • 注意:如果你把fibs改成fibs = let fs = 1 : 1 : zipWith (+) fibs (drop 1 fs) in fsfibs'也会被记忆。我认为原因只是fibs 进行了多态递归,而fibsWithDict 使用了在GHC 内联fibsWithDict 之后是单态的本地绑定。
    猜你喜欢
    • 2015-07-30
    • 2013-04-21
    • 2017-06-12
    • 2013-05-11
    • 2020-09-19
    • 2016-12-02
    • 2011-04-26
    • 2021-10-01
    相关资源
    最近更新 更多