将Haskell数据类型转换为Java(面向对象)

7
我是一名有用的助手,会进行文本翻译。
我试图将一个简单的Haskell数据类型和一个函数转换为面向对象。但我感到困惑。
下面是用于算术计算的Haskell类型:
data Expr = Lit Int | 
      Add Expr Expr |
   deriving Show

--Turn the expr to a nice string
showExpr :: Expr -> String 
showExpr (Lit n) = show n
showExpr (Add e1 e2) = "(" ++ showExpr e1 ++ "+" ++ showExpr e2 ++ ")"

现在,我正在尝试将...转换为...
public interface Expr {
  String showExpr(String n);
} 

// Base case
public class Lit implements Expr {
  String n;

  public Lit(String n) {
    this.n = n;
  }

  @Override
  public String ShowExpr() {
    return n;
  }
}

public class Add implements Expr {
  Expr a;
  Expr b;

  public Add(Expr aExpr, Expr bExpr) {
    this.a = aExpr;
    this.b = bExpr;
  }

  public String ShowExpr() {
    return "(" + a.ShowExpr() + "+" + b.ShowExpr() + ")";
  }

  public static void main(String[] args) {
    Lit firstLit  = new Lit("2");
    Lit secLit = new Lit("3");
    Add add = new Add(firstLit,secLit);
    System.out.println(add.ShowExpr());
  }
}

这将得到“(2+3)”的正确答案。但是,我不确定这是否是正确的思路和OO模型。这是否是Haskell数据类型的良好表示?

3
我认为你的转换非常不错。我唯一能提出的意见是Expr应该是一个抽象类而不是接口。另外,不要使用Java来解决这种问题,因为Haskell在这方面更加出色。 - nist
4个回答

5

让我们尽可能地复制代码。

以下是Haskell数据结构具有的一些属性:

  1. 它具有类型Expr和两个构造函数LitAdd
  2. 您无法从“外部”添加或删除构造函数

因此,如果我们希望这些属性在Java版本中保持不变,应该这样做:

public abstract class Expr {
    // So that you can't add more subclasses outside this block
    private Expr() {}

    // Simulate pattern matching:
    // (This CAN be done with instanceof, but that's ugly and not OO)
    public boolean isLit() {
        return false;
    }
    public boolean isAdd() {
        return false;
    }
    public Lit asLit() {
        throw new UnsupportedOperationException("This is not a Lit");
    }
    public Add asAdd() {
        throw new UnsupportedOperationException("This is not an Add");
    }

    public static class Lit extends Expr {
        public final int n;
        public Lit(int n) {
            this.n = n;
        }
        @Override
        public boolean isLit() {
            return true;
        }
        @Override
        public Lit asLit() {
            return this;
        }
    }

    public static class Add extends Expr {
        public final Expr a, b;
        public Lit(Expr a, Expr b) {
            this.a = a;
            this.b = b;
        }
        @Override
        public boolean isAdd() {
            return true;
        }
        @Override
        public Add asAdd() {
            return this;
        }
    }
}

现在,要转换showExpr
public static String showExpr(final Expr expr) {
    if(expr.isLit()) {
        return Integer.toString(expr.asLit().n);
    } else if(expr.isAdd()) {
        return "(" + expr.asAdd().a + "+" + expr.asAdd().b + ")";
    }
}

您可以将showExpr作为Expr类中的静态方法。我不会将其作为实例方法,因为那样会偏离Haskell版本的更远。

5
我认为if(e.isLit()) { Lit l = e.asLit(); }if(e instanceof Lit) { Lit l = (Lit)e; }并没有真正的区别。 像Haskell风格的模式匹配本质上不是面向对象的,因此,试图使用模拟instanceof和转换的方法来隐藏这一事实对代码没有任何帮助。 - yshavit
@yshavit,你当然是正确的,但我认为我提出的风格会导致更易于阅读的代码,并且减少像if(foo instanceof Foo) { ((Bar)foo).bla(); }这样的由于重构(Foo != Bar)而产生的错误。例如,我可能还想将AddLit变成私有类并为它们使用工厂方法,在这种情况下,instanceof将无法工作。 - dflemstr
无论是 e instanceof Lit 还是 e.isLit() 更易读,这取决于个人观点。但请注意,如果它们是私有类,则 e.asLit().n 也无法工作。 - yshavit

2
我会稍微改变一下这个想法:每个Haskell类都成为Java接口,而instance...where(或者derives)则变成了implements
因此,class Show a变成了:
interface Showable {
    String show();
}

那么,Expr数据类型是什么呢?它将各种Haskell构造器(也称为Java类)组合成一个单一的类型,并且还表示它们都由Haskell类(即实现Java接口)覆盖。因此,我会说数据类型Expr变成了:

interface Expr extends Showable {}

例如,Lit 构造函数变成了:

class Lit implements Expr {
    @Override
    String show() {
        ...
    }
}

现在,您可以拥有包含LitAdd实例(例如List<Expr>)的Java集合,并且对于任何Expr,您都知道它implements Showable
请注意,通过将Showable作为自己的接口(而不是直接将其方法放在Expr上),我允许其他完全不相关的Java类也实现它。这类似于任何人都可以定义您定义的Haskell类的instance ... where
最后,这里有一些开放世界和封闭世界政策之间的不匹配。Haskell的instance ... where是开放世界的,而Java的implements是封闭的。但是,Haskell的每种类型的构造函数是封闭的,而Java的extends大多数情况下是开放的。
关于implements是封闭的,您不能做太多事情,但您可以使用抽象类和私有构造函数关闭extends,因此如果您想要那种行为,那就是我会做的事情。稍微调整一下上面的内容,我们得到:
public abstract class Expr implements Showable {
    private Expr() {
        // hide the ctor from the outside world, so nobody else
        // can extend this class
    }

    public static class Lit extends Expr {
        ...
    }

    public static class Add extends Expr {
        ...
    }
}

如果原始代码使用了类型类,则这样做是可以的,但我认为这对于建模简单的ADT来说有点过头。 - Landei

1
我会这样翻译它:
public abstract class Expr {

    public static Expr Lit(final int n) {
        return new Expr() {
            public String showExpr() {
                return "" + n;
            }
        };
    }

    public static Expr Add(final Expr e1, final Expr e2) {
        return new Expr() {
            public String showExpr() {
                return String.format("(%s+%s)", e1.showExpr(), e2.showExpr());
            }
        };
    }

    public abstract String showExpr();

    public static void main(String[] args) {
        Expr firstLit = Lit(2);
        Expr secLit = Lit(3);
        Expr add = Add(firstLit, secLit);
        System.out.println(add.showExpr());
    }
}

在Haskell中,LitAdd不是子类型(在Haskell中没有这样的东西),而只是Expr表达式。在Java中没有必要公开子类,这就是为什么我可以在某些方法中使用匿名类隐藏它们。这很完美地工作,只要您不需要模式匹配(在Java中很难建模,因为简单的instanceof测试会在更复杂的示例中很快变得混乱)。

0

你的代码看起来没问题,但是可以更加习惯化。例如,你可以使用 final 让字段不可变。你也可以用 toString 来替换 showExptoString 已在 Object 中声明),除非你想让 toString 执行不同的操作。在我看来,使用 String.format 比连接多个字符串更简洁一些。最后,我建议将 Lit 存储为 int,因为它更紧凑且类型安全。

以下是我的翻译:

abstract class Exp {
  // Force subclasses to override Object.toString.
  public abstract String toString();
}

class Lit extends Exp {
  final int value;

  Lit(int value) {
    this.value = value;
  }

  public String toString() {
    return Integer.toString(value);
  }
}

class Add extends Exp {
  final Exp a, b;

  Add(Exp a, Exp b) {
    this.a = a;
    this.b = b;
  }

  public String toString() {
    return String.format("(%s + %s)", a, b);
  }
}

class Main {
  public static void main(String[] args) {
    Lit firstLit = new Lit(2);
    Lit secLit = new Lit(3);
    Add add = new Add(firstLit, secLit);
    System.out.println(add);
  }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接