您如何用C#或类似Java的语言编码代数数据类型?


58

代数数据类型很容易解决一些问题,例如,列表类型可以非常简洁地表示为:

data ConsList a = Empty | ConsCell a (ConsList a)

consmap f Empty          = Empty
consmap f (ConsCell a b) = ConsCell (f a) (consmap f b)

l = ConsCell 1 (ConsCell 2 (ConsCell 3 Empty))
consmap (+1) l

这个特定的例子在Haskell中,但是在其他语言中,本机支持代数数据类型,这将是相似的。

事实证明,有一个明显的面向对象样式子类型的映射:数据类型成为抽象基类,每个数据构造函数都成为具体的子类。这是Scala中的示例:

sealed abstract class ConsList[+T] {
  def map[U](f: T => U): ConsList[U]
}

object Empty extends ConsList[Nothing] {
  override def map[U](f: Nothing => U) = this
}

final class ConsCell[T](first: T, rest: ConsList[T]) extends ConsList[T] {
  override def map[U](f: T => U) = new ConsCell(f(first), rest.map(f))
}

val l = (new ConsCell(1, new ConsCell(2, new ConsCell(3, Empty)))
l.map(1+)

天真的子类之外,唯一需要的是密封类的方法,即一种不可能将子类添加到层次结构的方法。

您将如何使用C#或Java这样的语言解决这个问题?我在C#中尝试使用代数数据类型时发现了两个绊脚石:

  • 我不知道在C#中到底该调用什么类型(即我不知道要放入什么class Empty : ConsList< ??? >
  • 我想不出一种密封 的方法,ConsList因此无法将任何子类添加到层次结构中

在C#和/或Java中实现代数数据类型的最惯用的方法是什么?或者,如果不可能的话,什么是惯用的替代品?



3
C#是OOP语言。使用OOP解决问题。不要尝试使用任何其他范例。
欣快的2012年

7
@Euphoric C#在C#3.0中已成为一种非常有用的功能语言。一流的功能,内置的常用功能操作,单子。
Mauricio Scheffer 2012年

2
@Euphoric:有些领域很容易用对象建模而很难用代数数据类型建模,有些则相反。知道如何做都可以为您建模领域提供更大的灵活性。就像我说的那样,将代数数据类型映射到典型的OO概念并不那么复杂:数据类型成为抽象的基类(或接口或抽象的特征),数据构造函数成为具体的实现子类。这为您提供了开放的代数数据类型。继承限制为您提供了封闭的代数数据类型。多态性使您可以区分大小写。
约尔格W¯¯米塔格

3
@Euphoric,范式,schmaradigm,谁在乎?ADT与功能编程(或OOP或其他任何功能)正交。如果没有适当的ADT支持,对任何一种语言的AST进行编码都是很痛苦的,而如果没有另一个与范式无关的特征(模式匹配),那么编译该语言就很麻烦。
SK-logic

Answers:


42

有一种简单但繁琐的方式来密封Java中的类。您在基类中放置了一个私有构造函数,然后将其子类作为内部类。

public abstract class List<A> {

   // private constructor is uncallable by any sublclasses except inner classes
   private List() {
   }

   public static final class Nil<A> extends List<A> {
   }

   public static final class Cons<A> extends List<A> {
      public final A head;
      public final List<A> tail;

      public Cons(A head, List<A> tail) {
         this.head = head;
         this.tail = tail;
      }
   }
}

调整访客模式以进行调度。

我的项目jADT:Java Algebraic DataTypes会为您生成所有样板https://github.com/JamesIry/jADT


2
不知何故我的名字突然出现在这里!谢谢,我不知道这个成语。
约尔格W¯¯米塔格

4
当您说“样板沉重”时,我已经做好了应对更糟糕的事情的准备;-)有时Java对样板可能会很糟糕。
约阿希姆·绍尔

但这并不能构成:您没有必须通过
强制转换

不幸的是,这似乎无法表示一些更复杂的和类型,例如Either。看到我的问题
Zoey Hewll '18

20

您可以通过使用visitor模式来实现,该模式将补充模式匹配。例如

data List a = Nil | Cons { value :: a, sublist :: List a }

可以用Java编写为

interface List<T> {
    public <R> R accept(Visitor<T,R> visitor);

    public static interface Visitor<T,R> {
        public R visitNil();
        public R visitCons(T value, List<T> sublist);
    }
}

final class Nil<T> implements List<T> {
    public Nil() { }

    public <R> R accept(Visitor<T,R> visitor) {
        return visitor.visitNil();
    }
}
final class Cons<T> implements List<T> {
    public final T value;
    public final List<T> sublist;

    public Cons(T value, List<T> sublist) {
        this.value = value;
        this.sublist = sublist;
    }

    public <R> R accept(Visitor<T,R> visitor) {
        return visitor.visitCons(value, sublist);
    }
}

密封是由Visitor全班实现的。它的每个方法都声明如何解构一个子类。您可以添加更多的子类,但是它必须实现accept并通过调用visit...方法之一来实现,因此它的行为必须类似于Cons或like Nil


13

如果您滥用C#命名参数(在C#4.0中引入),则可以使代数数据类型易于匹配:

Either<string, string> e = MonthName(2);

// Match with no return value.
e.Match
(
    Left: err => { Console.WriteLine("Could not convert month: {0}", err); },
    Right: name => { Console.WriteLine("The month is {0}", name); }
);

// Match with a return value.
string monthName =
    e.Match
    (
        Left: err => null,
        Right: name => name
    );
Console.WriteLine("monthName: {0}", monthName);

这是Either该类的实现:

public abstract class Either<L, R>
{
    // Subclass implementation calls the appropriate continuation.
    public abstract T Match<T>(Func<L, T> Left, Func<R, T> Right);

    // Convenience wrapper for when the caller doesn't want to return a value
    // from the match expression.
    public void Match(Action<L> Left, Action<R> Right)
    {
        this.Match<int>(
            Left: x => { Left(x); return 0; },
            Right: x => { Right(x); return 0; }
        );
    }
}

public class Left<L, R> : Either<L, R>
{
    L Value {get; set;}

    public Left(L Value)
    {
        this.Value = Value;
    }

    public override T Match<T>(Func<L, T> Left, Func<R, T> Right)
    {
        return Left(Value);
    }
}

public class Right<L, R> : Either<L, R>
{
    R Value { get; set; }

    public Right(R Value)
    {
        this.Value = Value;
    }

    public override T Match<T>(Func<L, T> Left, Func<R, T> Right)
    {
        return Right(Value);
    }
}

我之前已经看过该技术的Java版本,但是lambda和命名参数使其更具可读性。+1!
2014年

1
我认为这里的问题是Right在错误类型上不是通用的。类似于:class Right<R> : Either<Bot,R>,其中的一个都更改为具有协变(输出)类型参数的接口,而Bot是底部类型(其他所有类型的子类型,与Object相对)。我认为C#没有底部类型。
croyd

5

在C#中,您不能使用该Empty类型,因为由于版本化,不同成员类型的基本类型不同。你只能有Empty<T>; 没有那么有用。

在Java中,Empty : ConsList由于类型擦除,您可能会遇到这种情况,但是我不确定类型检查器是否不会在某处尖叫。

但是,由于两种语言都有null,您可以它们的所有引用类型都认为是“ Whatever | Null”。因此,您只需将其null用作“空”,以避免必须指定其派生对象。


问题null在于它太笼统了:它表示不存在任何东西,即通常为,但是我想表示不存在列表元素,尤其是空列表。空列表和空树应具有不同的类型。同样,空列表需要是一个实际值,因为它仍然具有自己的行为,因此它需要具有自己的方法。要构建列表[1, 2, 3],我想说Empty.prepend(3).prepend(2).prepend(1)(或用具有右联想运算符的语言1 :: 2 :: 3 :: Empty),但是我不能说null.prepend …
约尔格W¯¯米塔格

@JörgWMittag:null确实具有不同的类型。为此,您还可以轻松创建带有null值的类型化常量。但是确实不能在上面调用方法。如果没有特定于元素类型的Empty,则使用方法的方法将无法正常工作。
Jan Hudec 2012年

一些狡猾的扩展方法可以伪造对null的“方法”调用(当然,它们都是静态的)
jk。

如果需要,可以使用Emptyand和Empty<>and滥用隐式转换运算符,以进行相当实际的模拟。本质上,您Empty在代码中使用,但是所有类型签名等仅使用通用变体。
伊蒙·纳邦

3

天真的子类之外,唯一需要的是密封类的方法,即一种不可能将子类添加到层次结构的方法。

在Java中,您不能。但是您可以将基类声明为私有包,这意味着所有直接子类都必须与基类属于同一包。如果随后将子类声明为final,则不能再对它们进行子类化。

我不知道这是否可以解决您的实际问题...


我没有真正的问题,或者我会将其发布在StackOverflow上,而不是在这里:-)代数数据类型的一个重要属性是可以关闭它们,这意味着个案数是固定的:在此示例中,列表要么为空,要么为空。如果可以静态地确保确实如此,则可以intanceof通过简单地确保始终执行“ 强制伪类型安全” 来进行动态强制转换或动态检查(即:即使编译器不知道,我也很安全)。检查这两种情况。但是,如果其他人添加了新的子类,那么我会遇到我没有想到的运行时错误。
约尔格W¯¯米塔格

@JörgWMittag-好吧,Java显然不支持它……从您似乎想要的强烈意义上来说。当然,您可以执行各种操作来阻止运行时不必要的子类型化,但是随后您会收到“意外的运行时错误”。
斯蒂芬·

3

数据类型ConsList<A>可以表示为接口。该接口公开了一个deconstruct方法,该方法使您可以“解构”该类型的值-即处理每个可能的构造函数。对deconstruct方法的调用类似于case ofHaskell或ML中的表单。

interface ConsList<A> {
  <R> R deconstruct(
    Function<Unit, R> emptyCase,
    Function<Pair<A,ConsList<A>>, R> consCase
  );
}

deconstruct方法对ADT中的每个构造函数都采用“回调”功能。在我们的情况下,它针对空列表情况采用一个函数,对于“ cons cell”情况采用另一个函数。

每个回调函数都接受构造函数接受的值作为参数。因此,“空列表”情况不带任何参数,而“ cons cell”情况则带两个参数:列表的开头和结尾。

我们可以使用Tuple类或使用currying 对这些“多个参数”进行编码。在此示例中,我选择使用一个简单的Pair类。

该接口为每个构造函数实现一次。首先,我们有“空列表”的实现。该deconstruct实现仅调用emptyCase回调函数。

class ConsListEmpty<A> implements ConsList<A> {
  public ConsListEmpty() {}

  public <R> R deconstruct(
    Function<Unit, R> emptyCase,
    Function<Pair<A,ConsList<A>>, R> consCase
  ) {
    return emptyCase.apply(new Unit());
  }
}

然后我们类似地实现“ cons cell”的情况。这次,该类具有属性:非空列表的头和尾。在deconstruct实现中,这些属性将传递给consCase回调函数。

class ConsListConsCell<A> implements ConsList<A> {
  private A head;
  private ConsList<A> tail;

  public ConsListCons(A head, ConsList<A> tail) {
    this.head = head;
    this.tail = tail;
  }

  public <R> R deconstruct(
    Function<Unit, R> emptyCase,
    Function<Pair<A,ConsList<A>>, R> consCase
  ) {
    return consCase.apply(new Pair<A,ConsList<A>>(this.head, this.tail));
  }
}

这是使用这种ADT编码的示例:我们可以编写一个reduce通常折叠列表的函数。

<T> T reduce(Function<Pair<T,A>,T> reducer, T initial, ConsList<T> l) {
  return l.deconstruct(
    ((unit) -> initial),
    ((t) -> reduce(reducer, reducer.apply(initial, t.v1), t.v2))
  );
}

这类似于Haskell中的此实现:

reduce reducer initial l = case l of
  Empty -> initial
  Cons t_v1 t_v2  -> reduce reducer (reducer initial t_v1) t_v2

有趣的方法,非常好!我可以看到与F#活动模式和Scala提取器的连接(不幸的是,那里可能也有指向Haskell Views的链接)。我没想到要将数据构造函数上的模式匹配责任转移到ADT实例本身中。
约尔格W¯¯米塔格

2

天真的子类之外,唯一需要的是密封类的方法,即一种不可能将子类添加到层次结构的方法。

您将如何使用C#或Java这样的语言解决这个问题?

这样做不是一个好方法,但是如果您愿意忍受骇人听闻的黑客攻击,则可以向抽象基类的构造函数中添加一些显式类型检查。在Java中,这类似于

protected ConsList() {
    Class<?> clazz = getClass();
    if (clazz != Empty.class && clazz != ConsCell.class) throw new Exception();
}

在C#中,由于使用通用泛型,因此更为复杂-最简单的方法可能是将类型转换为字符串并进行处理。

请注意,在Java中,即使是理论上也可以通过真正希望通过序列化模型或的人来绕过这种机制sun.misc.Unsafe


1
在C#中不会更加复杂:Type type = this.GetType(); if (type != typeof(Empty<T>) && type != typeof(ConsCell<T>)) throw new Exception();
svick 2012年

@svick,观察得很好。我没有考虑到基本类型将被参数化。
彼得·泰勒

辉煌!我想这足以进行“手动静态类型检查”。我更希望消除诚实的编程错误,而不是恶意的意图。
约尔格W¯¯米塔格
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.