2012-02-03 36 views
5

我在ocaml的递归不可改变的数据结构可以简化为这样的事:扩展不变类型(或:恒定类型快速缓存)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 

现在假设我映射到另一个表达式,我需要不断地检查一个expr的结果,有时不止一次同EXPR,有时为表达式最近使用模式

{ someExpr with eexpr = TSum(someExpr, otherExpr) } 

现在映射,结果功能是非常轻巧,但运行它多次为深AST不会非常优化。我知道我可以使用Hashtbl来缓存这个值,但是AFAIK Hashtbl只会做结构上的平等,所以它无论如何都需要遍历我的长期AST。 我知道最好的选择是在expr类型中包含一个可能不可变的“结果”字段。但我不能。

那么Ocaml有什么方法可以将一个值缓存到一个不可变的类型,所以我不需要每次需要时都急切地计算它呢?

谢谢!

回答

4

您可以使用函子接口来控制散列表使用的相等类型。我相信(==)的语义对您而言是合法的;即如果A == B,则对于任何纯函数f,f A = f B。所以你可以缓存fA的结果。然后如果你发现一个物理上等于A的B,缓存的值对于B是正确的。

使用(==)散列的缺点是散列函数会将所有结构相同的对象发送到同一个哈希桶,在这里它们将被视为不同的对象。如果表格中有许多结构相同的对象,则散列法不会从中受益。行为退化为线性搜索。

您无法定义哈希函数以使用物理地址,因为物理地址可以随时由垃圾收集器更改。

但是,如果您知道您的表格只包含相对较少的大数值,则使用物理平等可能适用于您。

+0

谢谢,Jeffrey为此作出了巨大的回应!所以最有可能我可以做一个列表和一个函数,将查找与==然后列表相同的行为? 我在ocaml手册上读到物理相等对不可变结构有未定义的行为,虽然它保证当A == B时A = B(当然)。当我使用模式{expr与eexpr = TAdd(expr,otherExpr)}时,是否保证在TAdd(thisExpr,_)thisExpr == expr? – Waneck 2012-02-03 11:26:00

+0

散列表比列表工作得更好,因为它将按近似的结构相等性(即通常的散列函数)排序为分箱。除非你的所有值在结构上相等,否则这比只使用一个列表要好。 (你可以定制你的散列函数来查看最经常变化的部分。)就像我说的,我认为(==)不变值的语义对你而言是可以的。对于什么是(==)没有强有力的保证,运行时允许用纯粹的值任意地巧妙(FP是如此酷的一个原因)。但在实践中,我会说是。 – 2012-02-03 17:13:11

+0

哦,我明白了!但是,每次哈希值都需要遍历大的AST,不是吗?我的“结果”函数是相当轻量级的,所以也许它比慢慢地调用它要慢,不是吗? – Waneck 2012-02-03 17:42:20

5

哈希值为expr_expr。通过这样做,程序中结构相同的值将共享完全相同的内存表示,并且可以用物理相等(==)替代结构相等(=)。

这个paper应该让你快速开始在OCaml的散列考虑。

+0

哈希缺点是一个伟大的功能!我不知道它存在!但是我的AST有一些非常复杂的值,就像我提到的那个“a_complex_type”。它有懒惰的值,函数,通常没有办法与a_complex_type进行比较。散列缺点会在这种情况下工作吗? – Waneck 2012-02-03 11:17:44

+0

另外它很可能不可能在我的AST(它也包含位置位置)中找到相同的值,但是当我使用构造{expr与eexpr = TAdd(expr,otherExpr)}时,在我看来,has-cons在那里没有必要。但这是一本很好的,内容翔实的论文。谢谢! 现在,我读了ocaml物理平等在不可变结构上有未定义的行为。像@Jeffrey提出的那样,没有安全漏洞可以安全使用吗?这应该可能够了! – Waneck 2012-02-03 11:20:36

+0

关于你输入的事实比这更复杂。如果它的所有组成部分都可以用散列构造函数构造,那么这不会成为问题。位置信息是有问题的。没有这一点,你可以用一个弱哈希表来轻松地记忆'result'函数,也可以用于递归调用的中间结果。 – 2012-02-03 12:57:56

1

我认为你可以合并上述两个想法:使用类似散列的技术来获取数据的“纯表达式”部分的散列,并将该散列用作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 
+0

这是一个很好的建议,但不幸的是,我的数据被存储的方式我不认为我可以将纯表达式从大多数独特的数据中分离出来,因为它们是相互递归的结构 – Waneck 2012-02-04 17:58:44

+0

@Waneck:我添加了一个实现分离的小实现来展示我的想法。 – gasche 2012-02-05 09:04:07