Caching 在OCaml中扩展不可变类型(或:不可变类型的快速缓存)

Caching 在OCaml中扩展不可变类型(或:不可变类型的快速缓存),caching,hashtable,ocaml,immutability,Caching,Hashtable,Ocaml,Immutability,我在ocaml中有一个递归的不可变数据结构,可以简化为如下内容: type expr = { eexpr : expr_expr; some_other_complex_field : a_complex_type; } and expr_expr = | TInt of int | TSum of (expr * expr) | TMul of (expr * expr) 这是一个AST,有时它会变得非常复杂(非常深) 有一个对表达式求值的递归函数。

我在ocaml中有一个递归的不可变数据结构,可以简化为如下内容:

type expr =
{
    eexpr : expr_expr;
    some_other_complex_field : a_complex_type;
}

and expr_expr =
    | TInt of int
    | TSum of (expr * expr)
    | TMul of (expr * expr)
这是一个AST,有时它会变得非常复杂(非常深)

有一个对表达式求值的递归函数。比如说,

let rec result expr =
    match expr.eexpr with
        | TInt i -> i
        | TSum (e1, e2) -> result e1 + result e2
        | TMul (e1, e2) -> result e1 * result e2
现在假设我正在将一个表达式映射到另一个表达式,我需要不断地检查一个表达式的结果,有时对同一个表达式检查多次,有时对最近使用该模式映射的表达式检查多次

{ someExpr with eexpr = TSum(someExpr, otherExpr) }
现在,result函数非常轻量级,但是多次运行它进行深度AST将不会得到很好的优化。我知道我可以使用Hashtbl缓存该值,但Hashtbl只能实现结构相等,因此无论如何它都需要遍历我的long-AST。 我知道最好的选择是在expr类型中包含一个可能不可变的“result”字段。但我不能

那么在Ocaml中有没有办法将一个值缓存到一个不可变的类型中,这样我就不必在每次需要时都急切地计算它了


谢谢

散列限制
expr\u expr
的值。通过这样做,程序中的结构相等值将共享完全相同的内存表示,并且可以用物理相等(
=
)替换结构相等(
=


这将使您快速开始在OCaml中考虑散列

可以使用函数接口控制哈希表使用的相等类型。我相信(=)的语义对于您的目的是合法的;i、 例如,如果A==B,那么对于任何纯函数f,fa=fb。因此,您可以缓存fa的结果。然后,如果您找到一个物理上等于A的B,那么缓存的值对于B是正确的

使用(==)进行散列的缺点是,散列函数会将所有结构上相等的对象发送到同一个散列桶,在那里它们将被视为不同的对象。如果表中有很多结构相同的对象,那么哈希就没有任何好处。该行为退化为线性搜索

您不能定义哈希函数来处理物理地址,因为垃圾收集器可以随时更改物理地址


但是,如果您知道您的表只包含相对较少的大ish值,那么使用物理相等可能适合您。

我认为您可以合并上述两种想法:使用类似哈希的技术获得数据“纯表达式”部分的哈希,并将此散列用作
eval
函数的记忆表中的键

当然,这只适用于
eval
函数确实只依赖于函数的“纯表达式”部分的情况,如您给出的示例所示。我认为这是一个相对普遍的情况,至少如果您限制自己存储成功的评估(例如,不会返回包含一些位置信息的错误)

编辑:一个小的概念证明:

type 'a _expr =
  | Int of int
  | Add of 'a * 'a

(* a constructor to avoid needing -rectypes *)
type pure_expr = Pure of pure_expr _expr

type loc = int
type loc_expr = {
  loc : loc;
  expr : loc_expr _expr;
  pure : pure_expr (* or any hash_consing of it for efficiency *)
}

(* this is where you could hash-cons *)
let pure x = Pure x

let int loc n =
  { loc; expr = Int n; pure = pure (Int n) }
let add loc a b =
  { loc; expr = Add (a, b); pure = pure (Add(a.pure, b.pure)) }

let eval =
  let cache = Hashtbl.create 251 in
  let rec eval term =
    (* for debug and checking memoization *)
    Printf.printf "log: %d\n" term.loc;
    try Hashtbl.find cache term.pure with Not_found ->
      let result =
        match term.expr with
          | Int n -> n
          | Add(a, b) -> eval a + eval b in
      Hashtbl.add cache term.pure result;
      result
  in eval



let test = add 3 (int 1 1) (int 2 2)
# eval test;;
log: 3
log: 2
log: 1
- : int = 3
# eval test;;
log: 3
- : int = 3

哈希是一个伟大的功能!我不知道它的存在!但是我的AST有一些非常复杂的值,比如我提到的“a_complex_type”。它有惰性的值、函数,通常无法与复杂类型的结构相等进行比较。散列cons在这种情况下有效吗?在我的AST中很可能找不到相同的值(它也包含位置位置),但是当我使用构造{expr with eexpr=TAdd(expr,otherExpr)}时,我觉得has cons在那里是不必要的。但这是一篇伟大的、内容丰富的论文。谢谢现在,我已经读到ocaml物理相等在不可变结构上有未定义的行为。像@Jeffrey建议的那样,在没有犯罪的情况下使用它安全吗?这应该足够了!考虑到你的打字比这更复杂。如果它的所有成分都可以用hash consed构造函数构造,那么这就不会是问题。职位信息实际上是有问题的。如果没有这一点,您可以使用弱哈希表轻松地记忆
result
函数,也可以用于递归调用的中间结果。是的,我希望我可以更改结构以使用hash cons!但是非常感谢你的建议!谢谢Jeffrey的回复!因此,我很可能可以对一个列表和一个函数执行相同的操作,该函数将使用==查找列表,然后呢?我在ocaml手册中读到,物理等式对于不可变结构具有未定义的行为,尽管可以保证当A==B时,A=B(当然)。当我使用模式{expr with eexpr=TAdd(expr,otherExpr)}时,是否可以保证在TAdd(thisExpr,u)thisExpr==expr)中,哈希表比列表工作得更好,因为它将按照近似的结构等式(即,通常的哈希函数)排序到容器中。除非所有值在结构上都相等,否则这比只使用一个列表要好。(你可以定制你的散列函数来查看最常不同的部分。)就像我说的,我认为(=)对于不可变值的语义适合你的目的。对于什么是(==)到什么,没有强有力的保证,运行时可以任意使用纯值(FP如此酷的一个原因)。但我会说是的,在实践中。哦,我明白了!但是它必须在每次散列比较时遍历大AST,不是吗?我的“result”函数相当轻量级,所以它可能比急切地调用它要慢,不是吗?我正在考虑使用Hashtbl的位置,然后线性搜索。这对我来说是最好的,但遗憾的是,这是我正在处理的应用程序的具体设计。哈希函数不必遍历整个结构。这只是一个近似值。事实上,预定义的哈希函数不会遍历整个结构。您可以编写一个哈希函数,它可以执行任何您喜欢的操作(通过