[关闭]
@juda 2017-12-25T16:22:43.000000Z 字数 5456 阅读 608

Reading Notes about Expression Problem


In the past week, I read the papers you gave me. Basically, I understand the background, motivation and approaches. Here is some notes.

1. Background

Expression problem, by definition, is to define a data type by cases, where one can add new cases to the data type and new functions over the data type, without recompiling existing code, and while retaining static type safety.

In OOP language such as JAVA, we can use a example showed below to describe the problem.

Assume we want to implement a AST which supports literal and add(+) operation, one may write the code:

  1. interface Exp {int eval(); }
  2. class Lit implements Exp {
  3. int x;
  4. public Lit(int x){this.x = x; }
  5. public int eval() {return x; }
  6. }
  7. class Add implements Exp {
  8. Exp l, r;
  9. public Add(Exp l, Exp r) {this.l = l; this.r = r; }
  10. public int eval() {return l.eval() + r.eval(); }
  11. }

If we try to add a new variant such as times(*) operation, it's quiet easy.

  1. class Mul implements Exp {
  2. Exp l, r;
  3. public Mul(Exp l, Exp r) {this.l = l; this.r = r; }
  4. public int eval() {return l.eval() * r.eval(); }
  5. }

However, once we are about to add a new operation like render, we have to modify existing code. Below is the example, the line 1, 6, 12 and 19 should be added to the old code.

  1. interface Exp {int eval(); String render();}
  2. class Lit implements Exp {
  3. int x;
  4. public Lit(int x){this.x = x; }
  5. public int eval() {return x; }
  6. public String render() {return "Int "+x; }
  7. }
  8. class Add implements Exp {
  9. Exp l, r;
  10. public Add(Exp l, Exp r) {this.l = l; this.r = r; }
  11. public int eval() {return l.eval() + r.eval(); }
  12. public String render() {return "( "+l.render()+" + "+r.render()+" )"; }
  13. }
  14. class Mul implements Exp {
  15. Exp l, r;
  16. public Mul(Exp l, Exp r) {this.l = l; this.r = r; }
  17. public int eval() {return l.eval() * r.eval(); }
  18. public String render() {return "( "+l.render()+" * "+r.render()+" )"; }
  19. }

However, for most function programming languages, they face different challenge to this problem. Take Haskell as example.

  1. data Exp = Lit Int | Add Exp Exp
  2. eval :: Exp -> Int
  3. eval (Lit n) = n
  4. eval (Add x y) = eval x + eval y

Above is the initial code. If we are going to add a operation, that's quiet simple.

  1. render :: Exp -> String
  2. render (Lit n) = "Int " ++ n
  3. render (Add x y) = "( " ++ render x ++ " + " ++ render y ++ " )"

In contrast to OOP, once we want to add a new variant, we must change the exisiting code. The final code is showed below.

  1. data Exp = Lit Int | Add Exp Exp | Mul Exp Exp
  2. eval :: Exp -> Int
  3. eval (Lit n) = n
  4. eval (Add x y) = eval x + eval y
  5. eval (Mul x y) = eval x * eval y
  6. render :: Exp -> String
  7. render (Lit n) = "Int " ++ n
  8. render (Add x y) = "( " ++ render x ++ " + " ++ render y ++ " )"
  9. render (Mul x y) = "( " ++ render x ++ " * " ++ render y ++ " )"

2. Object Algebras

In 2012, a lightweight design pattern for OOP was came up. It utilizes the abstract factory. The original paper also relates it to visitor pattern, but avoid using accept method, or we might think it provides concrete internal visitor implementations.

Below is the example code I write, we can find it's simple but effective. Besides, since this approach combines the features from abstract factory and visitor pattern, we could do more than it, like multi-type, multi-class, etc.

  1. // ==== initial AST ====
  2. interface IntAlg<A> {
  3. A lit(int n);
  4. A add(A l, A r);
  5. }
  6. interface Exp {
  7. int eval(); // regards it as concrete internal visitor
  8. }
  9. class IntFactory implements IntAlg<Exp> {
  10. public Exp lit(int n) {
  11. return new Exp() {
  12. int eval() {return n;}
  13. }
  14. public Exp add(Exp l, Exp r) {
  15. return new Exp() {
  16. int eval() {return l.eval() + r.eval();}
  17. }
  18. }
  19. }
  20. // ==== initial AST ====
  21. // ==== add variant ====
  22. interface MulAlg<A> extends IntAlg<A> {
  23. A mul(A l, A r)
  24. }
  25. class MulFactory extends IntFactory implements MulAlg<Exp> {
  26. public Exp mul(Exp l, Exp r) {
  27. return new Exp() {
  28. int eval() {return l.eval() * r.eval();}
  29. }
  30. }
  31. }
  32. // ==== add variant ====
  33. // === add operation ===
  34. /**
  35. * This code shows modularity
  36. */
  37. interface Render{
  38. String render();
  39. }
  40. class IntRenderFactory implements IntAlg<Render> {
  41. public Render lit(int n) {
  42. return new Render() {
  43. int render() {return "Int "+n; }
  44. }
  45. public Render add(Exp l, Exp r) {
  46. return new Render() {
  47. int render() {return "( "+l.render()+" + "+r.render()+" )"; }
  48. }
  49. }
  50. }
  51. /**
  52. * Without retroactive implementation
  53. * Here we direct define the render operation.
  54. */
  55. class DirectRender implements IntAlg<String> {
  56. public String lit(int n) {
  57. return "Int "+n;
  58. }
  59. public String add(Exp l, Exp r) {
  60. return "( "+l.render()+" + "+r.render()+" )";
  61. }
  62. }
  63. // === add operation ===

3. Data types

In my view, this method to EP is relatively straightforward.

  1. ---- initial AST ----
  2. data Expr f = In ( f ( Expr f ) )
  3. --In is a function we do not define here
  4. data Val e = Val Int
  5. type IntExpr = Expr Val
  6. data Add e = Add e e
  7. type AddExpr = Expr Add
  8. ---- initial AST ----

From the code above, one finding is that we have a mapping to combine this two different type into one "type" then we can solve this problem easily. Hence, the author mention the coproduct.

By coproduct of functor, one can unify the different types. More importantly, coproduct could be implement to more than two types.

However, it is because coproduct is key idea, one must ensure the types are disjoint, which limits the extensibility of coproduct.

4. Disjoint Intersection Types and Polymorphism

In 2012, Dunfield proposed a simply typed core calculus with intersection types and a merge operator, which may provide an alternative to replace coproduct. However, it is incoherence, in order words, when applying a functor to this type, the behavior is undefined, so you cannot foresee the result.

The paper Disjoint Intersection Types reviews some solutions to this issue and gives its idea. Firstly, it defined a type system which is disjoint. However, it cannot deal with both function and argument are disjoint intersection type. Thus authors add a type limit, i.e. bi-directional type-checking.

Compared with the last paper, the paper Disjoint Polymorphism is the extension of disjoint intersection types.

5. Thought

From my perspective, object algebras is a nice OO approach to solve EP. Coproduct is another method to EP but more related to FP. Due to the limit of coproduct, we want to replace it by another type system and extend its expressivity.

For me, the difficult for me is the background because they are not textbooks. I have some trouble understanding some notation and I am not familiar to proofs.

I believe there are still lots of things to do with the disjoint interaction types especially when it relates to subtype and inheritance. As the authors said, The naive addition of new features seems to be problematic.

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注