Rascal 树上的顺序累积操作

Sequential, cumulative operations on Rascal trees

考虑到 Rascal 中数据的不变性,如果后面的操作依赖于前面的结果,那么首选的方法是什么?

例如,为树中的每个节点分配注释值,其中较高节点的值取决于较低节点的值。如果您编写包含多个案例的单个访问语句,则较低级别的插入语句不会更改树,因此较高级别的操作可能没有任何可操作的内容。另一方面,用访问语句围绕每个 case 语句——并在每次访问后将树变量重新绑定到新树——很麻烦,而且更糟的是,似乎使结果取决于语句的顺序。

微妙的问题 :-) visit 语句具有微妙的语义,特别是如果您从 OO 的角度来看它。在主动遍历一个值的同时,它也在重建一个新值。根据其 case 语句 "see" 中不同事物的遍历顺序匹配的策略(顺序)。

有时将 visit 想象成一个代码生成器是很有见地的,它生成一组相互递归的函数,这些函数将被访问的部分作为参数,return return 上的新值。访问的主体(案例)变成了 switch 语句,它是这些生成函数的核心。然后,根据遍历顺序,递归步骤要么位于 (bottom-up) 之前,要么位于此 switch 语句之后 (top-down):

// bottom-up visit
x f(value x) {
  newChildren = map(f, children of x);
  x = newX(x, newChildren);
  switch (x) {
    case y : return whatever(y);
  }
}

因此 bottom-up 访问的 switch case 中的代码看到递归调用生成的树(尽管实际上没有更新树)。

这是一个例子:

rascal>data T = tee(T, T) | i(int i);
data T = tee(T, T) | i(int i);
ok
rascal>t = tee(tee(i(1),i(2)),tee(i(3),i(4)));
t = tee(tee(i(1),i(2)),tee(i(3),i(4)));
T: tee(
  tee(
    i(1),
    i(2)),
  tee(
    i(3),
    i(4)))
rascal>visit(t) { 
>>>>>>>  case i(x)        => i(x+1) 
>>>>>>>  case tee(i(x),y) => tee(i(x+1),y) 
>>>>>>>}
T: tee(
  tee(
    i(3),   // here you see how 1 turned into 3 by incrementing twice
    i(3)),  // increment happened once here
  tee(
    i(5),   // increment happened twice here too
    i(5)))  // increment happened once here

你可以看到一些节点有两次增量,因为第二种情况匹配 一个 tee 在它的子 i 节点已经被访问到 return 另一个已经递增的 i 节点之后。

试验其他策略会得到其他结果,请参阅 http://tutor.rascal-mpl.org/Rascal/Rascal.html#/Rascal/Expressions/Visit/Visit.html。请注意,访问语句范围内的变量由所有递归级别上的所有访问共享,这使得能够模拟类似拉链的行为(您始终可以将先前访问的节点存储在临时文件中)。

顺便说一句,语言设计试图避免需要更多复杂的东西 "functional programming design patterns",例如 zippers,因为它们会使类型系统和人们与之交互的方式复杂化。为了使这些东西在对异构数据类型进行递归的访问中正确工作,您需要多态性博士学位才​​能理解它何时是正确的类型。秘密地,visit 语句模拟了一组内置的类型安全的 rank-2 高阶多态函数,但这都是在幕后。