Little Languages 2

Reading 28: 小语言2

软件6031


安全漏洞                                                               易于理解                                                                                           准备改变


在今天和未知的未来,都是正确的。                与未来以后的程序员进行清晰的沟通,也包括将来的你。                    构造适应变化而不重写。

 

目标

本阅读的目的是介绍访问者模式,将函数视为一级值的另一个示例,以及递归数据类型操作的替代实现策略

访问者是实现为递归数据类型的小语言的一个常见特性。

 

递归类型的函数

自从我们介绍递归数据类型以来,我们一直在使用解释器模式对这些类型实现函数:

  · 在定义数据类型的接口中将操作声明为实例方法,并在每个具体变量中实现该操作。

就复合模式而言,复合变体(例如音乐中的Concat,而不是公式中的Concat)将递归地实现操作,而基本变体(例如音乐中的音符,公式中的变量)将实现基本情况。

例如,下面是布尔公式递归数据类型:

Formula = Variable(name:String)
          + Not(formula:Formula)
          + And(left:Formula, right:Formula)
          + Or(left:Formula, right:Formula)

假设我们要向公式中添加一个操作变量,它返回公式中提到的所有变量名的集合。我们在递归数据类型的阅读练习中看到了如何做到这一点(与此同时,使用不可变集类型插入)。

阅读练习

  实现变量操作

我们将通过修改接口以指定它来实现该操作:

public interface Formula {
    // ... other operations ...
    /** @return all the variables that appear in this Formula */
    public Set<String> variables();
}

修改所有变量类,实现基本情况:

public class Variable implements Formula {
    // ... name field, constructor, other operations ...
    @Override public Set<String> variables() {
        return singletonSet(this.name);
    }
}

和递归的情况:

public class Not implements Formula {
    // ... formula field, constructor, other operations ...
    @Override public Set<String> variables() {
    }
}
public class And implements Formula {
    // ... left and right fields, constructor, other operations ...
    @Override public Set<String> variables() {
        return setUnion(this.left.variables(), this.right.variables());
    }
}
public class Or implements Formula {
    // ... left and right fields, constructor, other operations ...
    @Override public Set<String> variables() {
        return setUnion(this.left.variables(), this.right.variables());
    }
}

在这段代码中,singletonSet是单个元素集的想象创建者,setUnion构造其输入集的并集。

让我们考虑这种方法的两个缺点

  1. 对于具有许多变体的数据类型的复杂操作,实现该操作的代码将分布在所有不同的变体类中。如果要检查所有代码,对其进行重构或修复错误,则必须在所有这些不同的地方找到它:代码更难理解。我们的递归实现中的错误更易于引入且更难以发现。

  2. 如果我们希望在类型中添加新操作,则必须修改接口和每个实现类。如果有许多已经具有大量代码的具体变体,可能是由其他程序员编写和维护的,则与我们将新操作的代码保留在其单独位置相比,要进行此更改将更加困难。

模式匹配

回到您在递归数据类型阅读练习中编写的函数定义:

variables(Variable(x)) = set-add(x, set-empty())
variables(Not(f))      = variables(f)
variables(And(f1, f2)) = set-union(variables(f1), variables(f2))
variables(Or(f1, f2))  = set-union(variables(f1), variables(f2))

从上面将这些案例直接转换为我们类中的代码,我们想编写如下代码:

public static Set<String> variables(Formula f) {
    switch(f) {
    case Variable var:
        return singletonSet(var.name);
    case Not not:
        return variables(not.formula);
    case And and:
        return setUnion(variables(and.left), variables(and.right));
    case Or or:
        return setUnion(variables(or.left), variables(or.right));
    }
}

即使抛开于每种变体的代表直接连接,switch这样的结构也不是有效的Java。有很多语言支持实例的模式匹配来确定其底层结构,但是Java并不是其中一种。

Java中的错误模式匹配

如果我们试图将上面的代码转换成可工作的Java,我们可能会写出这样一个可怕的东西:

public static Set<String> variables(Formula f) {
    if (f instanceof Variable) {
        return singletonSet(((Variable)f).name);
    }
    if (f instanceof Not) {
        return variables(((Not)f).formula);
    }
    if (f instanceof And) {
        And and = (And)f;
        return setUnion(variables(and.left), variables(and.right));
    }
    if (f instanceof Or) {
        Or or = (Or)or;
        return setUnion(variables(or.left), variables(or.right));
    }
    throw new IllegalArgumentException("don't know what " + f + " is");
}

这个代码很糟糕,甚至还把代表曝光放在一边。静态检查已经被抛出了窗口:我们根据我们知道的所有具体变量检查输入f的类型,并在找到匹配项时进行转换。编译器没有检查我们的工作。这段代码不安全。

阅读练习

别扔给我
扔进另一个

 

递归类型上的函数

要找到采用另一种方法的方法,请考虑我们想要的:递归数据类型上的函数。与其将功能视为一段代码,考虑将函数表示为数据。

我们的示例操作具有类型签名:

变量:公式→设置<字符串>

我们是否可以使用Function标准库中的类型,该类型表示单个参数的功能,并由参数类型和返回类型进行参数化?

Function<Formula, Set<String>> variables = f -> {
    // ... did that help?
};

这对我们没有任何帮助。lambda的主体仍然只知道f是一个公式,这还不足以实现提取变量的适当递归或基本情况。

让我们尝试定义一个新类型,专门用于表示函数Formula

/**
 * Represents a function on different kinds of Formulas
 * @param <R> the type of the result of the function
 */
interface FormulaFunction<R> {

    /**  @return this applied to var */
    public R onVariable(Variable var);

    /**  @return this applied to not */
    public R onNot(Not not);

    /**  @return this applied to and */
    public R onAnd(And and);

    /**  @return this applied to or */
    public R onOr(Or or);
}

现在将变量操作定义为实现的类FormulaFunction

class VariablesInFormula implements FormulaFunction<Set<String>> {

    // The base case works nicely:

    @Override public Set<String> onVariable(Variable var) {
        return singletonSet(var.name);
    }

    // But we're not sure how to implement recursive calls:

    @Override public Set<String> onNot(Not not) {
        return ???; // how to apply this to not.formula?
    }

    @Override public Set<String> onAnd(And and) {
        return setUnion(???); // how to apply this to and.left & and.right?
    }

    @Override public Set<String> onOr(Or or) {
        return setUnion(???); // how to apply this to or.left & or.right?
    }
}

这段代码中,onNot、onAnd和onOr将很容易编写,但只有在我们了解如何进行递归调用并将函数(this)应用于它们的参数的公式子级之后。如果我们能回答这个问题,我们还将回答如何使用这些变量之一的问题­在­首先是公式对象:

Formula f = ...;
VariablesInFormula getVariables = new VariablesInFormula();
Set<String> theVariables = ...; // how to call getVariables on f?

我们怎样才能从调用一些方法去Formula目标-像f在这段代码,或像and.leftor.right在前面的代码-调用我们需要的特定变体的方法?Java中方法调用的动态分派可以帮助我们实现目标。动态分派为熟悉的解释器模式提供了动力:如果在Formula接口中定义了一个方法,则可以在实现的任何具体类型的实例上调用它Formula,而运行的代码就是该具体类型的实现。

双调度模式,我们将出动两次:第一次到具体的变型(例如 AndOr等等),然后递归功能(例如, VariablesInFormula或其他FormulaFunction)。

第一个方法调用用作客户端通过以下方式调用其功能的入口点:

public interface Formula {

    // ... other operations ...

    /**
     * Call a function on this Formula.
     * @param <R> the type of the result
     * @param function the function to call
     * @return function applied to this
     */
    public <R> R callFunction(FormulaFunction<R> function);
}

我们将callFunction在每个变体中实现此操作以进行第二个函数调用,每个具体变体现在负责将其实际类型透露给Formula­Function

阅读练习

实现callFunction

public class Variable implements Formula {
    // ... fields, other methods, etc. ...
    
    @Override public <R> R callFunction(FormulaFunction<R> function) {
        return function.onVariable(this);
    }
}
public class Not implements Formula {
    // ... fields, other methods, etc. ...
    
    @Override public <R> R callFunction(FormulaFunction<R> function) {
        return function.onNot(this);
    }
}
public class And implements Formula {
    // ... fields, other methods, etc. ...
    
    @Override public <R> R callFunction(FormulaFunction<R> function) {
    }
}
public class Or implements Formula {
    // ... fields, other methods, etc. ...
    
    @Override public <R> R callFunction(FormulaFunction<R> function) {
    }
}

因此,对callFunction的调用(由所有四个变量实现)会立即返回,并调用传入函数的相应onVariant方法。现在我们可以在变量中完成递归实现­在­公式,并称之为:

阅读练习

实施VariablesInFormula

class VariablesInFormula implements FormulaFunction<Set<String>> {
    
    @Override public Set<String> onVariable(Variable var) {
        return singletonSet(var.name);
    }
    
    @Override public Set<String> onNot(Not not) {
    }
    
    @Override public Set<String> onAnd(And and) {
        return setUnion(and.left.callFunction(this),
                        and.right.callFunction(this));
    }
    
    @Override public Set<String> onOr(Or or) {
        return setUnion(or.left.callFunction(this),
                        or.right.callFunction(this));
    }
}

调用VariablesInFormula

Formula f = ...;
VariablesInFormula getVariables = new VariablesInFormula();
Set<String> theVariables = ??? ;

访问者

假设我们表示公式(P∨Q)∧¬R。按照数据类型定义,结构为:

And( Or(Variable("P"), Variable("Q")),
     Not(Variable("R")) )

以f那个Formula实例。

阅读练习

f的实际类型是什么?

如果我们运行f.callFunction(getVariables),执行将如何进行?

f.callFunction runs VariablesInFormula.onAnd
and.left.callFunction runs VariablesInFormula.onOr
or.left.callFunction runs VariablesInFormula.onVariable
returns the set { "P" }
or.right.callFunction runs VariablesInFormula.onVariable
returns the set { "Q" }
returns setUnion{ "P" }, { "Q" } = { "P", "Q" }
and.right.callFunction runs VariablesInFormula.onNot
not.formula.callFunction runs VariablesInFormula.onVariable
returns the set { "R" }
returns the set { "R" } unchanged
returns setUnion({ "P", "Q" }, { "R" }) = { "P", "Q", "R" }

我们用FormulaFunction类型和callFunction操作创建的是一种特殊的迭代器,它遍历公式的树结构,一次处理一个树中的节点。此模式的名称为visitor:

  • 访客实现switch了各种类型:上面实现的代码Variables­In­Formula非常接近我们声明的目标switch,但是每种情况都有其自己的方法。onVariant

  • 访客代表递归类型上的函数:我们可以创建特定对象的实例Formula­Function,将其传递,根据需要应用它,依此类推。如果要代表的函数需要其他参数,则可以使它们成为构造函数的参数,将这些值存储在我们要引用的字段中。

  • 访问者充当迭代器:诸如变量之类的函数的实现者并未编写显式的控制结构来一一拉出树中的每个节点。取而代之的是,实现者callFunction(this)对这些Formula变量进行递归调用,并依赖于其变体来对其进行回调。

我们将对术语进行一些更改,以获取Formula访问者的最终版本:

  • 我们将其称为Formula.Visitor而不是FormulaFunction
  • 在实例上绕过访问者称为接受它,因此我们将其重命名callFunctionaccept
  • 在visitor界面中,由于每个方法都采用不同的静态类型,因此我们可以为所有这些方法使用单个重载名称onVarianton

最后,让我们停止直接引用我们的字段,并实现访问者可以使用的观察者操作。我们不需要将这些操作添加到Formula界面中:它们是特定于变量的,并且仅由知道我们具体类型是什么的客户端使用。

public interface Formula {

    public interface Visitor<R> {
        public R on(Variable var);
        public R on(Not not);
        public R on(And and);
        public R on(Or or);
    }

    public <R> R accept(Visitor<R> visitor);

    // ... other operations ...
}

class Variable implements Formula {
    // ...
    public String name() { ... }
    @Override public <R> R accept(Visitor<R> visitor) {
        return visitor.on(this);
    }

}
class Not implements Formula {
    // ...
    public Formula formula() { ... }
    @Override public <R> R accept(Visitor<R> visitor) {
        return visitor.on(this);
    }
}
class And implements Formula {
    // ...
    public Formula left() { ... }
    public Formula right() { ... }
    @Override public <R> R accept(Visitor<R> visitor) {
        return visitor.on(this);
    }
}
class Or implements Formula {
    // ...
    public Formula left() { ... }
    public Formula right() { ... }
    @Override public <R> R accept(Visitor<R> visitor) {
        return visitor.on(this);
    }
}

class VariablesInFormula implements Formula.Visitor<Set<String>> {
    @Override public Set<String> on(Variable var) {
        return singletonSet(var.name());
    }
    @Override public Set<String> on(Not not) {
        return not.formula().accept(this);
    }
    @Override public Set<String> on(And and) {
        return setUnion(and.left().accept(this), and.right().accept(this));
    }
    @Override public Set<String> on(Or or) {
        return setUnion(or.left().accept(this), or.right().accept(this));
    }
}

调用它:

Formula f = ...;
Set<String> variables = f.accept(new VariablesInFormula());

为什么是访客?

考虑将实现以下所有操作的所有代码Formula

 多变的不是其他的
变量:公式→设置 <String>    
否定法线:公式→公式    
评价:公式×环境→布尔值    
        ......    

并比较解释器模式和访问者模式,以在递归数据类型上实现功能:

口译员:代码按列分组。每个变体都有该变体的所有代码,实现了所有操作。

游客:代码按行分组。一个操作的所有代码都将驻留在一个访问者类中,并带有处理所有变体的代码。

解释器模式使添加新变体变得更加容易,因为我们不必更改任何现有代码:我们只需要在新变体类中将所有各种操作实现为方法即可。

解释器模式使添加新变体变得更加容易,因为我们不必更改任何现有代码:我们只需要在新变体类中将所有各种操作实现为方法即可。

访问者模式使添加新操作更加容易。无需修改接口和每个变体类,我们只需要创建一个新的(例如Formula.Visitor实现,并为我们的新操作添加所有代码。现有的类或接口没有任何变化。

因此,如果添加新操作是我们最想做的更改,则我们可以选择定义一个访问者界面,并将我们的功能编写为访问者。 

当我们希望类型的客户了解并理解其底层结构并实现其自己的操作时,我们还将使用访问者模式-并且我们已经看到了一个常见且重要的用例:解析树!查看我们编写的讲具体语法树转换为抽象语法树的代码,请注意一个臭模式:

static IntegerExpression makeAST(ParseTree<IntegerGrammar> parseTree) {
    switch (parseTree.name()) {
    case ROOT:
        // ... call makeAST recursively on child ...
    case SUM:
        // ... iterate over children and create Plus expressions ...
    case PRIMARY:
        // ... call makeAST recursively on child ...
    case NUMBER:
        final int n = Integer.parseInt(parseTree.text());
        return new Number(n);
    default:
        throw new AssertionError("should never get here");
}

概括

访客模式克服了Java语言中缺少的功能,从而为我们提供了一种类型安全的方式,可以将递归数据类型上的功能表示和实现为自包含模块。访问者实例也是功能对象,因此我们可以自然地将它们作为值传递给我们的程序。

如果我们希望将来在递归ADT中添加新的操作,则使用visitor可以使我们的代码可以随时更改,而不会牺牲静态检查提供的错误的安全性。

如果我们正在构建一种ADT设计的语言,目的是允许客户从我们的实现类之外对我们的类型定义自己的操作,那么访问者将使这成为可能。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值