How to implement Exp in Bool or Iff 来自论文可扩展性

How to implement Exp in Bool or Iff from the paper Extensibility for the Masses

我目前正在阅读论文 大众的可扩展性。对象代数的实用可扩展性,Bruno C. d. S. Oliveira 和 William R. Cook(可在互联网上的许多地方找到 - 例如此处:https://www.cs.utexas.edu/~wcook/Drafts/2012/ecoop2012.pdf)。

在第 10 页,他们写道:

Adding new data variants is easy. The first step is to create new classes Bool and Iff in the usual object-oriented style (like Lit and Add):

class Bool implements Exp {...}
class Iff implements Exp {...}

Exp 的实现似乎留给了 reader 作为练习。然而,我不清楚 Exp 在本文的这一部分是如何定义的。我的问题是:

BoolIff应该如何实现?

这是我试过的方法:

Exp

的第一个定义

在论文的开头,Exp接口是这样定义的:

interface Exp {
    Value eval();
}

这里,Value是由另一个接口定义的:

interface Value {
    Integer getInt();
    Boolean getBool();
}

然而,这篇论文很快就偏离了 Exp 的定义,转而支持基于访问者的定义。

Bool

的可能实现

根据该定义,应该如何实施 Bool class?

这样的事情似乎是一个开始:

class Bool implements Exp {
    boolean x;
    public Bool(boolean x) { this.x = x; }
    public Value eval() {
        return new VBool(x);
}}

然而,问题变成了如何正确实施 Value?

论文只显示了这个:

class VBool implements Value {...}

对我来说,实施似乎并不完整:

class VBool implements Value {
    boolean x;
    public VBool(boolean x) { this.x = x; }
    public Boolean getBool() {
        return new Boolean(x);
    }
    public Integer getInt() {
        // What to return here?
    }
}

正如我上面的尝试所示,不清楚从 getInt 到 return 的内容。我想我可以 return null 或抛出异常,但这意味着我的实现是 partial.

无论如何,Exp 的第一个定义似乎只是作为论文中的一个激励示例存在,然后继续定义更好的替代方案。

Exp

的第二个定义

论文第 4 页将 Exp 重新定义为内部访问者:

interface Exp {    
    <A> A accept(IntAlg<A> vis);
}

其中IntAlg<A>是另一个接口:

interface IntAlg<A> {
    A lit(int x);
    A add(A e1, A e2);
}

到目前为止,事情似乎很清楚,直到我们开始实施 BoolIff...

Bool

的可能实现

根据Exp的这个定义,我们应该如何实现提议的Boolclass?

class Bool implements Exp {
    boolean x;
    public Bool(boolean x) { this.x = x; }
    public <A> A accept(IntAlg<A> vis) {
        // What to return here?
}}

无法凭空变出 A 值,因此必须与 vis 交互才能产生 A 值。但是,vis 参数只定义了 litadd 方法。

lit 方法需要一个 int,它在 Bool 中不可用。

同样,add 需要 两个 A 值,这些值也不可用。再一次,我发现自己陷入了僵局。

Exp的第三个定义?

然后,在第 8 页,论文显示了这个例子:

int x = exp(base).eval();

这里,exp(base) returns Exp,但是eval是哪个定义呢?

显然,Exp 仍然(或再次?)有一个 eval 方法,但现在 return 是 int。看起来像这样吗?

interface Exp {
    int eval();
}

这篇论文没有给出这个定义,所以我可能误解了什么。

Bool

的可能实现

我们可以用 Exp 的定义来实现 BoolIff 吗?

class Bool implements Exp {
    boolean x;
    public Bool(boolean x) { this.x = x; }
    public int eval() {
        // What to return here?
}}

同样,不清楚如何实现该接口。当然,可以 return 0 代表 false,1 代表 true,但这只是一个任意决定。这似乎不合适。

我是否缺少 Exp 的第四个定义?还是论文中还有其他一些我没有理解的信息?

顺便说一句,如果我在尝试中犯了错误,我深表歉意。我一般不写Java代码。

@马克。让我试着澄清你的困惑点。

Exp 的定义

我们在第10页假设的Exp的定义是:

interface Exp {
    Value eval();
}

这在图 1 中的论文前面已经介绍过。可以忽略带有访问者的 Exp 的替代版本。我们只是用它来谈论访问者以及与Object Algebras的关系,它在论文的后面部分没有发挥作用。

另外,关于第8页的代码,我认为论文中有错别字。你是对的:我们似乎假设了一个 returns int 的 eval 方法。当我们写这篇论文时,我们使用了多个版本的 Exp,可能我们错误地使用了另一个版本的代码。第8页的代码,适配论文中的表述,应该是:

int x = exp(base).eval().getInt();

价值的选择

我们打算用于实现值 class 的代码使用了异常,类似于您自己的尝试,并且它确实如论文中所述是部分的。在本文中,我们提出的要点是关于源表达式的可扩展性和类型安全性。对于值,我们想要的只是值足够丰富以支持源表达式,并且假设值本身是不可扩展的(稍后在第 7 节中我们将简要讨论可扩展值)。我们在论文中选择的表示旨在保持代码简单,并避免在处理错误管理(与可扩展性正交)时分心。

同意这不是一个很好的代表,但至少对于当时的 Java 版本来说,这似乎是一个合理的妥协。在现代 Java 中,我认为要走的路是将 Value 建模为代数数据类型。 Java 16 支持 pattern matching and sealed types,可用于在函数式编程中对代数数据类型进行本质建模。所以你可以用类似的东西来模拟价值:

sealed interface Value {}
record VInt(int x) implements Value {}
record VBool(boolean b) implements Value {} 

回想起来,为了避免像您现在这样的混淆,我认为直接使用界面来展示论文会更好:

interface Exp {
    int eval();
}

并使用 if 构造 à la C,其中整数扮演布尔值的角色。这将首先避免对 Value 表示的干扰。

论文中值的表示

无论如何,回到论文的表述,以及你关于偏颇的观点,接下来我将展示代码的最小但完整的实现。为了说明偏颇不是根本问题,我也从使用未检查的异常切换到检查的异常。

首先我们可以定义如下值:

interface Value {
    // You can choose checked exceptions or unchecked exceptions.
    // In the paper we opted for unchecked exceptions.
    // But here, I show that you can also use checked exceptions,
    // if you want to ensure that you deal with exception cases.
    int intValue() throws Exception;
    boolean boolValue() throws Exception;
}

class VInt implements Value {
    int x;

    public VInt(int x) {
        this.x = x;
    }

    public int intValue() throws Exception {
        return x;
    }

    public boolean boolValue() throws Exception {
        throw new Exception();
    }

    public String toString() {
        return Integer.valueOf(x).toString();
    }
}

class VBool implements Value {
    boolean b;

    public VBool(boolean b) {
        this.b = b;
    }

    public int intValue() throws Exception {
        throw new Exception();
    }

    public boolean boolValue() throws Exception {
        return b;
    }

    public String toString() {
        return Boolean.valueOf(b).toString();
    }
}

然后你可以用对象代数写代码如下:

    // Integer Expressions
interface IntAlg<Exp> {
    Exp lit(int x);
    Exp add(Exp e1, Exp e2);
}

interface Exp {
    Value eval() throws Exception;
}

class IntEval implements IntAlg<Exp> {
    public Exp lit(int x) {
        return () -> new VInt(x);
    }

    public Exp add(Exp e1, Exp e2) {
        return () -> new VInt(e1.eval().intValue() + e2.eval().intValue());
    }
}

// Boolean Expressions
interface BoolAlg<Exp> extends IntAlg<Exp> {
    Exp bool(boolean b);
    Exp iff(Exp e1, Exp e2, Exp e3);
}

class BoolEval extends IntEval implements BoolAlg<Exp> {
    public Exp bool(boolean b) {
        return () -> new VBool(b);
    }

    public Exp iff(Exp e1, Exp e2, Exp e3) {
        return () -> e1.eval().boolValue() ? e2.eval() : e3.eval();
    }
}

public class Main {
    public static <Exp> Exp m(IntAlg<Exp> alg) {
        return alg.add(alg.lit(4),alg.lit(3));
    }

    public static <Exp> Exp m2(BoolAlg<Exp> alg) {
        return alg.iff(alg.bool(false),m(alg),alg.bool(true));
    }

    public static void main(String[] args) {
        Exp e = m(new IntEval());
        try {
            System.out.println(e.eval());
        } catch (Exception exp) {
            System.out.println("Ops! There is an error...");
        }

        Exp e2 = m2(new BoolEval());
        try {
            System.out.println(e2.eval());
        } catch (Exception exp) {
            System.out.println("Ops! There is an error...");
        }
    }
}

正如您在 main 的定义中看到的,我们最终需要处理异常。事实上,如果你想更好地处理错误,你应该更改解释器中的代码以捕获那里的异常,然后给出错误消息,如“你不能将布尔值添加到整数......”和很快。这确实涉及额外的代码(这与技术的要点没有太大关系),但可以轻松完成。

另请注意,我使用 lambda 来避免使用匿名 classes 的一些样板。在 Java 中,具有单个方法实现的匿名 classes 可以写成 lambda。

如果您不喜欢使用异常的代码,并且更喜欢更函数式的方法,您也可以使用 Java 的可选 class 来模拟失败(就像你会用像 Haskell 这样的函数式语言来做)。在这种情况下,您可以使用以下内容:

// Values
interface Value {
    Optional<Integer> intValue();
    Optional<Boolean> boolValue();
} 

// The expression interface
interface Exp {
    Optional<Value> eval();
}

我们现在使用 Optional 而不是部分操作。使用此选项的 Java 代码(没有模式匹配)不会那么好,但也许使用新的模式匹配功能它不会那么糟糕。