表达式问题的三种解法

本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。

本文稍后将同步发布于知乎

本文基于

[1]Wadler, P.: The Expression Problem. Email (Nov 1998), discussion on the JavaGenericity mailing list

[2]Zenger, M., Odersky, M.: Independently extensible solutions to the expression prob-lem. In: FOOL’05 (2005)

[3]Oliveira, B.C.d.S., Cook, W.R. (2012). Extensibility for the Masses. In: Noble, J. (eds) ECOOP 2012 – Object-Oriented Programming. ECOOP 2012. Lecture Notes in Computer Science, vol 7313. Springer, Berlin, Heidelberg. doi:10.1007/978-3-642-31057-7_2

[4]SWIERSTRA, W. (2008). Data types à la carte. Journal of Functional Programming, 18(4), 423-436. doi:10.1017/S0956796808006758

[5]Bahr, P. (2014). Composing and decomposing data types. Proceedings of the 10th ACM SIGPLAN Workshop on Generic Programming, p. 71-82, 2014. https://doi.org/10.1145/2633628.2633635

[6]Carette, J., Kiselyov, O., Shan, Cc. (2007). Finally Tagless, Partially Evaluated. In: Shao, Z. (eds) Programming Languages and Systems. APLAS 2007. Lecture Notes in Computer Science, vol 4807. Springer, Berlin, Heidelberg. https://doi.org/10.1007/978-3-540-76637-7_15

[7]Kiselyov, O. (2012). Typed Tagless Final Interpreters.In: Gibbons, J. (eds) Generic and Indexed Programming. Lecture Notes in Computer Science, vol 7470. Springer, Berlin, Heidelberg. https://doi.org/10.1007/978-3-642-32202-0_3

[8]Kiselyov, O., & Ishii, H. (2015a). Freer Monads, More Extensible Effects. Proceedings of the 2015 ACM SIGPLAN Symposium on Haskell, 94–105. Presented at the Vancouver, BC, Canada. doi:10.1145/2804302.2804319

表达式问题

如何在一个类型内部的 representation 和外部的 behavior 均根据需求不断修改、增加,而无需重新编译乃至修改原有代码的问题称为表达式问题(Expression Problem, EP)[Wadler, 1998]

要求:

  • 双向可扩展性:解决方案必须允许添加新的数据变体和新的操作
  • 无修改或重复:现有代码不得被修改或重复
  • 分离编译和类型检查:安全检查或编译步骤不得推迟到链接或运行时进行
  • 独立可扩展性:不同的拓展可以组合使用[Zenger & Odersky, 2005]

在 OOP 中

interface Exp { int eval(); }

class Lit implements Exp {
    int x;
    Lit(int x) { this.x = x; }
    public int eval() { return x; }
}

class Add implements Exp {
    Exp l, r;
    Add(Exp l, Exp r) { this.l = l; this.r = r; }
    public int eval() { return l.eval() + r.eval(); }
}

public class Main {
    public static void main(String[] args) {
        Exp e = new Add(new Lit(1), new Lit(2));
        System.out.println(r.eval());
    }
}

添加新的 data constructor 是简单的:

interface Exp { int eval(); }

class Lit implements Exp {
    int x;
    Lit(int x) { this.x = x; }
    public int eval() { return x; }
}

class Add implements Exp {
    Exp l, r;
    Add(Exp l, Exp r) { this.l = l; this.r = r; }
    public int eval() { return l.eval() + r.eval(); }
}

class Mult implements Exp {
    Exp l, r;
    Mult(Exp l, Exp r) { this.l = l; this.r = r; }
    public int eval() { return l.eval() * r.eval(); }
}

public class Main {
    public static void main(String[] args) {
        Exp e = new Add(new Lit(1), new Lit(2));
        Exp r = new Mult(e, new Lit(4));
        System.out.println(r.eval());
    }
}

修改 representation 就不那么简单了:如果我们在 IExp 中添加一个格式化输出的方法,那么下面的每一个类都要修改添加对应的实现方法

解决方法(Object Algebra)

An object algebra is a class that implements a generic abstract factory interface, which corresponds to a particular kind of algebraic signature[Oliveira, 2005]

使用 Visitor Pattern

interface Exp { <A> A visit(Visitor<A> vis); }

interface Visitor<A> {
  A lit(Lit a);
  A add(Add a);
}

class Lit implements Exp {
  int value;
  public <A> A visit(Visitor<A> vis) { return vis.lit(this); }
}

class Add implements Exp {
  Exp left, right;
  public <A> A visit(Visitor<A> vis) {
    return vis.add(left.visit(vis), right.visit(vis));
  }
}

class Eval implements Visitor<Integer> {
  public Integer lit(Lit a) { return a.value; }
  public Integer add(Add a) { return a.visit(this) + b.visit(this); }
}

无论是加 data constructor 还是 method 都只需修改 visitor,原有的代码无需修改

interface Exp { <A> A visit(Visitor<A> vis); }

interface Visitor<A> {
    A lit(Lit a);
    A add(Add a);
    A mult(Mult a);
}

class Lit implements Exp {
    int value;
    public Lit(int value) { this.value = value; }
    public <A> A visit(Visitor<A> vis) { return vis.lit(this); }
}

class Add implements Exp {
    Exp left, right;
    public Add(Exp left, Exp right) { this.left = left; this.right = right; }
    public <A> A visit(Visitor<A> vis) {
        return vis.add(this);
    }
}

class Mult implements Exp {
    Exp left, right;
    public Mult(Exp left, Exp right) { this.left = left; this.right = right; }
    public <A> A visit(Visitor<A> vis) {
        return vis.mult(this);
    }
}

class Eval implements Visitor<Integer> {
    public Integer lit(Lit a) { return a.value; }
    public Integer add(Add a) { return a.left.visit(this) + a.right.visit(this); }
    public Integer mult(Mult a) { return a.left.visit(this) * a.right.visit(this); }
}

class PrettyPrint implements Visitor<String> {
    public String lit(Lit a) { return Integer.toString(a.value); }
    public String add(Add a) { return "(" + a.left.visit(this) + " + " + a.right.visit(this) + ")"; }
    public String mult(Mult a) { return "(" + a.left.visit(this) + " * " + a.right.visit(this) + ")"; }
}

例题

PatternCraft - Visitor

在 FP 中

在函数式语言中的情形恰好相反,我们可以很轻松地添加新的函数,却不能简单地修改数据的 representation

Haskell 没有 OOP,并不能使用 Object Algebra。但是我们有别的解决方案。

解决方法(Finally Tagless)

remove the tags

将原本的数据构造子写作 typeclass 中的 method,添加不同的操作就是为这个 typeclass 实现新的实例。

tagless 得名于我们最后的结果里用方法而非代数数据类型(ADT)来表示数据构造。

现在我们只需要标注类型就可调用不同的操作

这个实现依然存在问题,如下:

我们的语义阻止不了把布尔值相加这种不合法的操作

稍微改写一下,我们不直接修改最后的结果,而是多加一层 Functor

然后给每个想要的操作写一个 newtype

问题解决

除了直接计算,我们还可以构建语法树

解决方法(Data types à la carte)

Fixing

将我们的 constructor 拆分开来

首先我们需要表达任意次函数应用的嵌套。我们引入了 Fix 不动点来解决这个问题。

其中 In 的类型为 `f (Fix f) -> Fix f`, out 的类型为 `Fix f -> f (Fix f)`

有了 Fix 我们就可以表达树状结构的表达式

现在我们需要表达不同操作之间的组合,这就引入了 coproduct

两个 Functor 的 coproduct 还是 Functor

如果 f 是 Functor,我们可以折叠任意层数的 f (F-algebra : https://en.wikipedia.org/wiki/F-algebra)

其中 `foldFix f :: Fix f -> a`,`fmap (foldFix f) :: f (Fix f) -> f a`

foldFix 的第一个参数被称为 algebra,她每一次折叠一层 f a,而 foldFix 将这些操作应用到每一层

Evaluating

定义运算:

Automating Injection

现在这个写法很别扭,写一个表达式要套很多层 In。我们可以用自动化注入来解决这个问题。

:<: 称为 Subsume。 这一段 typeclass 定义了 Subtyping (sub 是 sup 的子类型,sup 是 sub 的父类型)。OOP 中提及到的多态基本都是子类型多态。一句话解释:subtyping 是类型上的偏序关系,如果 A 是 B 的子类型,那么 A 可以出现在任何 B 应该出现的位置。

补全偏序的性质如上。

接下来的操作就很简单了:

如上,我们不再需要手写 Fix 的结构,typeclass 会自动推断我们需要的类型然后注入。

拓展新的数据和新的操作十分简单:

PS: type family 的写法

上面用 typeclass 需要用到 overlapping instances,并且存在限制:f :<: g 中的 f 只能是 atomic 的,而 g 必须写成链表的形式,这意味着类似于以下合法的关系没有办法表示出来:

问题的根源在于 overlapping typeclass 没有优先级:正常 typeclass 会优先匹配更“精细”的实例,而当实例的“匹配度”一样的时候 GHC 就无法判断应该应用哪一个 instance。

解决方法是使用 closed type family[Bahr (2014)]:

Type family 是在 type-level 上的函数,把类型和其他类型关联到一起,如图:

这种运算在编译期完成。

考虑 f :<: g 表示在 g 中存在 f,我们只要表示出这个搜索的过程即可。

对上面三个问题的解决方法:

  1. 将 f 中多个成员拆开来分别在 g 中查找,然后用 sum 表示出来
  2. 分别搜索 g 的分支,并且对两个分支都出现 f 的情况予以拒绝

之后的用法和之前一致

例题

Codewars - Finally tagless interpreter

Codewars - Data Types à la Carte

更多阅读

The Expression Problem by Philip Wadler

Freer Monads, More Extensible Effects

全部评论
卧槽,楼主jetbrains的?
点赞 回复 分享
发布于 2023-05-05 11:35 北京
🤗
点赞 回复 分享
发布于 2023-05-05 11:08 北京

相关推荐

不愿透露姓名的神秘牛友
07-15 22:48
牛马人的牛马人生:建议就是把北邮几个字放大就行了。北邮本硕按理来说完全不用担心啊
点赞 评论 收藏
分享
评论
1
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务