我认为这有两个困难的部分:
- 一种非常高效的地图数据结构
- 一种高效的数据结构,用于跟踪要重新计算的内容
联合查找数据结构本身已经在 Prolog 中实现,但隐藏在“变量”这个有点晦涩的名称后面。如果您有办法将术语与表示它们的并集等价类的变量相关联,那么:
-
find 操作是查找类变量
- 测试两个类是否相同是
Class1 == Class2
-
union 操作是Class1 = Class2
所以find 是一个瓶颈。在命令式语言中,查找通常是常数时间,使用类似foo.equivalence_class 或equivalence_classes[foo.id] 的东西。通常,在 Prolog 中,我们没有等效的(近)恒定时间映射。但由于您似乎只对变量感兴趣,SWI-Prolog 的 attributed variables 确实符合要求!
我们可以为这样的术语列表计算联合查找等价类:
compute_classes([]).
compute_classes([Term | Terms]) :-
term_variables(Term, Variables),
variables_class(Variables, _NewEquivalenceClass),
compute_classes(Terms).
variables_class([], _EquivalenceClass).
variables_class([Var | Vars], EquivalenceClass) :-
( get_attr(Var, equivalence_class, ExistingEquivalenceClass)
-> ExistingEquivalenceClass = EquivalenceClass
; put_attr(Var, equivalence_class, EquivalenceClass) ),
variables_class(Vars, EquivalenceClass).
使用您的示例:
?- compute_classes([X+Y, Y+Z, T]).
put_attr(X, equivalence_class, _2772),
put_attr(Y, equivalence_class, _2772),
put_attr(Z, equivalence_class, _2772),
put_attr(T, equivalence_class, _2814).
我们可以看到X、Y 和Z 都共享一个等价类,而T 在一个单独的类中。
一些实用程序:
var_class(Var, Class) :-
get_attr(Var, equivalence_class, Class).
var_var_sameclass(Var1, Var2) :-
var_class(Var1, Class1),
var_class(Var2, Class2),
Class1 == Class2.
var_var_union(Var1, Var2) :-
var_class(Var1, Class1),
var_class(Var2, Class2),
Class1 = Class2.
继续举例:
?- compute_classes([X+Y, Y+Z, T]), var_class(X, ClassX), var_class(Y, ClassY), var_class(T, ClassT).
ClassX = ClassY,
put_attr(X, equivalence_class, ClassY),
put_attr(Y, equivalence_class, ClassY),
put_attr(Z, equivalence_class, ClassY),
put_attr(T, equivalence_class, ClassT).
?- compute_classes([X+Y, Y+Z, T]), var_var_sameclass(X, Y).
put_attr(X, equivalence_class, _3436),
put_attr(Y, equivalence_class, _3436),
put_attr(Z, equivalence_class, _3436),
put_attr(T, equivalence_class, _3478).
?- compute_classes([X+Y, Y+Z, T]), var_var_sameclass(X, T).
false.
?- compute_classes([X+Y, Y+Z, T]), var_var_union(Z, T), var_var_sameclass(X, T).
put_attr(X, equivalence_class, _3502),
put_attr(Y, equivalence_class, _3502),
put_attr(Z, equivalence_class, _3502),
put_attr(T, equivalence_class, _3502).
也就是说,X 和 Y 确实属于同一类,而 X 和 T 不是。如果我们合并Z 和T 的类,那么X 和T 会突然在同一个类中。
杀死变量是更乏味的地方。这里的想法(正如您在问题中所建议的那样)是仅重新计算输入的“受影响”部分。我认为这可以通过将一组受影响的术语与每个等价类相关联来完成。我将在这里使用列表,但我不建议在实践中使用列表是一个好的选择。
计算术语列表的等价类,以及每个等价类的“监视列表”:
compute_classes_and_watchlists(Terms) :-
compute_classes(Terms),
maplist(compute_watchlist, Terms).
compute_watchlist(Term) :-
term_variables(Term, [RepresentativeVariable | _OtherVars]),
var_class(RepresentativeVariable, Class),
( get_attr(Class, class_watchlist, Watchlist)
-> true
; Watchlist = [] ),
put_attr(Class, class_watchlist, [Term | Watchlist]).
例如:
?- compute_classes_and_watchlists([X+Y, Y+Z, T]).
put_attr(X, equivalence_class, _2932),
put_attr(_2932, class_watchlist, [Y+Z, X+Y]),
put_attr(Y, equivalence_class, _2932),
put_attr(Z, equivalence_class, _2932),
put_attr(T, equivalence_class, _3012),
put_attr(_3012, class_watchlist, [T]).
因此,如果您要杀死 X、Y 或 Z 中的任何一个,他们的类 _2932 的监视列表会告诉您,您需要重新计算术语 Y+Z 和X+Y(但没有别的)。
Killing 本身获取被杀死变量的类及其监视列表(它“返回”)并清除该类中每个变量的等价类:
kill_var(Var, TermsToRecompute) :-
var_class(Var, Class),
get_attr(Class, class_watchlist, TermsToRecompute),
del_attr(Class, class_watchlist),
maplist(clear_class, TermsToRecompute).
clear_class(Term) :-
term_variables(Term, [RepresentativeVariable | _OtherVars]),
del_attr(RepresentativeVariable, equivalence_class).
只有在您立即 (a) 将被杀死的变量绑定到基本术语,并且 (b) 重新计算受影响术语的等价类时,才有意义。在您的示例中:
?- compute_classes_and_watchlists([X+Y, Y+Z, T]), kill_var(Y, TermsToRecompute), Y = y_is_now_bound, compute_classes_and_watchlists(TermsToRecompute).
Y = y_is_now_bound,
TermsToRecompute = [y_is_now_bound+Z, X+y_is_now_bound],
put_attr(X, equivalence_class, _4640),
put_attr(_4640, class_watchlist, [X+y_is_now_bound]),
put_attr(Z, equivalence_class, _4674),
put_attr(_4674, class_watchlist, [y_is_now_bound+Z]),
put_attr(T, equivalence_class, _4708),
put_attr(_4708, class_watchlist, [T]).
这变得难以阅读,但关键是在杀死和绑定 Y 并在受影响的术语上重新计算 union-find 结构后,X 和 Z 现在位于不同的等价类中。 T 只是坐着不受影响。
所有这些都假设您不会在联合中回溯,尽管我对 SWI 的属性化 var 文档的阅读是,这甚至不会做任何非常错误的事情。由于put_attr 是可回溯的,因此需要更加小心,这可能会成为一个相当灵活的实现,其中回溯只是将类再次拆分。我认为,也可以让杀戮中的回溯也起作用。
待办事项:
- 在监视列表的情况下,必须更改
var_var_union 的定义以合并两个类的监视列表(如果它们不同);对于实际列表,这将是append,但是一些实际的集合或更专业的数据结构会更好,特别是如果您希望有某种“类似堆栈”的行为,其中下一个要被杀死的变量很可能是你最近做了一次union 操作
- 尤其是监视列表的方法,必须防止用户意外地统一
equivalence_class术语;这可以通过使用某种 class(<unique_id>, NakedClassVariable) 而不是仅仅使用裸变量来表示等价类来完成
- 对
term_variables 的重复调用可能会以某种方式进行优化 - 除了术语监视列表之外,您还可以保留感兴趣变量的监视列表
总而言之,这不是完全适合生产的代码,但它可能会给你一些想法。