如何添加仅缓存ADT的字段?

时间:2012-10-27 08:16:56

标签: design-patterns caching haskell memoization algebraic-data-types

我经常需要向ADT添加字段,只记忆一些冗余信息。但我还没有完全弄清楚如何做得好而有效。

显示问题的最佳方法是举个例子。假设我们正在使用无类型的lambda术语:

type VSym = String

data Lambda = Var VSym 
            | App Lambda Lambda
            | Abs VSym Lambda

我们有时需要计算一个术语的自由变量集:

fv :: Lambda -> Set VSym
fv (Var v)    = Set.singleton v
fv (App s t)  = (fv s) `Set.union` (fv t)
fv (Abs v t)  = v `Set.delete` (fv t)

很快我们意识到fv的重复计算是我们应用的瓶颈。我们想以某种方式将它添加到数据类型中。像:

data Lambda1 = Var (Set VSym) VSym
             | App (Set VSym) Lambda Lambda
             | Abs (Set VSym) VSym Lambda

但它使定义相当丑陋。几乎(Set VSym)比其他所有人都占用更多的空间。此外,它在使用Lambda的所有函数中打破了模式匹配。更糟糕的是,如果我们后来决定添加一些其他的memoizing字段,我们将不得不重新编写所有模式。

如何设计一个允许轻松且不引人注意地添加此类备忘录字段的通用解决方案?我希望达到以下目标:

  1. data定义应尽可能接近原始定义,以便易于阅读和理解。
  2. 模式匹配也应保持简单易读。
  3. 以后添加新的记忆字段不应该破坏现有代码,特别是:
    • 不打破现有模式,
    • 不要求使用我们想要记忆的函数的更改(例如在此示例中使用fv的代码)。

  4. 我将描述我当前的解决方案:为了使data定义和模式匹配尽可能地混乱,让我们定义:

    data Lambda' memo = Var memo VSym 
                      | App memo (Lambda' memo) (Lambda' memo)
                      | Abs memo VSym (Lambda' memo)
    type Lambda = Lambda' LambdaMemo
    

    其中要备份的数据是单独定义的:

    data LambdaMemo = LambdaMemo { _fv :: Set VSym, _depth :: Int }
    

    然后是一个检索memoized部分的简单函数:

    memo :: Lambda' memo -> memo
    memo (Var c _)   = c
    memo (App c _ _) = c
    memo (Abs c _ _) = c
    

    (这可以通过使用命名字段来消除。但是我们也可以have to name all the other fields。)

    这允许我们从memoize中选择特定部分,保持与fv相同的签名:

    fv :: Lambda -> Set VSym
    fv = _fv . memo
    
    depth :: Lambda -> Int
    depth = _depth . memo
    

    最后,我们声明了这些智能构造函数:

    var :: VSym -> Lambda
    var v = Var (LambdaMemo (Set.singleton v) 0) v
    
    app :: Lambda -> Lambda -> Lambda
    app s t = App (LambdaMemo (fv s `Set.union` fv t) (max (depth t) (depth s))) s t
    
    abs :: VSym -> Lambda -> Lambda
    abs v t = Abs (LambdaMemo (v `Set.delete` fv t) (1 + depth t)) v t
    

    现在我们可以有效地编写混合模式匹配的内容和读取记忆字段(如

    canSubstitute :: VSym -> Lambda -> Lambda -> Bool
    canSubstitute x s t
      | not (x `Set.member` (fv t))
          = True -- the variable doesn't occur in `t` at all
    canSubstitute x s t@(Abs _ u t')
      | u `Set.member` (fv s)
          = False
      | otherwise
          = canSubstitute x s t'
    canSubstitute x s (Var _ _)
          = True
    canSubstitute x s (App _ t1 t2)
          = canSubstitute x s t1 && canSubstitute x s t2
    

    这似乎解决了:

    • 模式匹配仍然很合理。
    • 如果我们添加一个新的memoizing字段,它将不会破坏现有代码。
    • 如果我们决定记下带有签名Lambda -> Something的函数,我们可以轻松地将其添加为新的记忆字段。

    我仍然不喜欢这个设计:

    • data定义并不是那么糟糕,但仍然会让memo处于混乱状态。
    • 我们需要使用智能构造函数来构造值,但我们使用常规构造函数进行模式匹配。这不是很糟糕,我们只需添加一个_,但具有相同的构造和解构签名会很好。我想ViewsPattern Synonyms会解决它。
    • memoized字段(自由变量,深度)的计算与智能构造函数紧密耦合。由于可以合理地假设这些记忆函数始终是catamorphisms,我相信这可以通过the fixpoint package等工具在某种程度上解决。

    任何想法如何改善它?或者有更好的方法来解决这个问题吗?

1 个答案:

答案 0 :(得分:2)

我认为通过在函数中使用普通的旧memoization而不是在ADT本身中缓存结果,可以实现所有目标。几周前,我发布了stable-memo包,这应该对此有所帮助。检查你的标准,我认为我们不能做得更好:

  1. 您的数据定义根本不会改变。
  2. 模式匹配也不会改变。
  3. 现有代码不必仅因为您编写更多记忆功能而改变。
    • 没有现有的模式被破坏。
    • 没有现有的记忆功能被破坏。
  4. 使用它非常简单。只需将memo应用于您要记忆的任何函数,确保在任何地方都使用该函数的memoized版本,即使在递归调用中也是如此。以下是如何编写您在问题中使用的示例:

    import Data.StableMemo
    
    type VSym = String
    
    data Lambda = Var VSym 
                | App Lambda Lambda
                | Abs VSym Lambda
    
    fv :: Lambda -> Set VSym
    fv = memo go
      where
        go (Var v)   = Set.singleton v
        go (App s t) = fv s `Set.union` fv t
        go (Abs v t) = v `Set.delete` fv t