Làm thế nào để bạn mã hóa các kiểu dữ liệu đại số bằng ngôn ngữ C # - hoặc Java?


58

Có một số vấn đề được giải quyết dễ dàng bằng các kiểu dữ liệu đại số, ví dụ như một loại Danh sách có thể được thể hiện rất ngắn gọn như sau:

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

Ví dụ cụ thể này là trong Haskell, nhưng nó sẽ tương tự trong các ngôn ngữ khác với sự hỗ trợ riêng cho các kiểu dữ liệu đại số.

Nó chỉ ra rằng có một ánh xạ rõ ràng đến phân nhóm kiểu OO: kiểu dữ liệu trở thành một lớp cơ sở trừu tượng và mọi hàm tạo dữ liệu trở thành một lớp con cụ thể. Đây là một ví dụ trong 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+)

Điều duy nhất cần thiết ngoài phân lớp ngây thơ là một cách để đóng dấu các lớp, tức là một cách để không thể thêm các lớp con vào một hệ thống phân cấp.

Làm thế nào bạn sẽ tiếp cận vấn đề này bằng một ngôn ngữ như C # hoặc Java? Hai khối vấp ngã tôi tìm thấy khi cố gắng sử dụng Kiểu dữ liệu đại số trong C # là:

  • Tôi không thể tìm ra loại dưới cùng được gọi trong C # (nghĩa là tôi không thể tìm ra loại gì để đưa vào class Empty : ConsList< ??? >)
  • Tôi không thể tìm ra một cách để đóng dấu ConsList sao cho không có lớp con nào có thể được thêm vào hệ thống phân cấp

Điều gì sẽ là cách thành ngữ nhất để triển khai các kiểu dữ liệu đại số trong C # và / hoặc Java? Hoặc, nếu không thể, sự thay thế thành ngữ là gì?



3
C # là ngôn ngữ OOP. Giải quyết vấn đề bằng OOP. Đừng thử sử dụng bất kỳ mô hình nào khác.
Euphoric

7
@Euphoric C # đã trở thành một ngôn ngữ chức năng khá hữu dụng với C # 3.0. Chức năng hạng nhất, tích hợp các hoạt động chức năng chung, đơn nguyên.
Mauricio Scheffer

2
@Euphoric: một số miền dễ mô hình hóa với các đối tượng và khó mô hình hóa với các kiểu dữ liệu đại số, một số miền thì ngược lại. Biết cách làm cả hai giúp bạn linh hoạt hơn trong việc mô hình hóa tên miền của mình. Và như tôi đã nói, ánh xạ các kiểu dữ liệu đại số vào các khái niệm OO điển hình không phức tạp: kiểu dữ liệu trở thành một lớp cơ sở trừu tượng (hoặc một giao diện hoặc một đặc điểm trừu tượng), các hàm tạo dữ liệu trở thành các lớp con triển khai cụ thể. Điều đó cung cấp cho bạn một kiểu dữ liệu đại số mở. Các hạn chế về thừa kế cung cấp cho bạn một kiểu dữ liệu đại số khép kín. Đa hình cho bạn trường hợp phân biệt đối xử.
Jörg W Mittag

3
@Euphoric, paradigm, schmaradigm, ai quan tâm? ADT là trực giao với lập trình chức năng (hoặc OOP hoặc bất cứ điều gì khác). Mã hóa AST của bất kỳ ngôn ngữ nào là một nỗi đau nếu không có sự hỗ trợ của các ADT đàng hoàng và việc biên dịch ngôn ngữ đó là một nỗi đau mà không có một đặc điểm bất khả tri nào khác, khớp mẫu.
SK-logic

Câu trả lời:


42

Có một cách dễ dàng, nhưng soạn sẵn các lớp nặng trong Java. Bạn đặt một hàm tạo riêng trong lớp cơ sở sau đó tạo các lớp con bên trong của nó.

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;
      }
   }
}

Tack trên một mẫu khách truy cập cho công văn.

Dự án của tôi jADT: Dữ liệu đại số Java tạo ra tất cả các mẫu soạn sẵn cho bạn https://github.com/JamesIry/jADT


2
Bằng cách nào đó tôi không ngạc nhiên khi thấy tên của bạn bật lên ở đây! Cảm ơn, tôi không biết thành ngữ này.
Jörg W Mittag

4
Khi bạn nói "nồi hơi nặng", đôi khi tôi đã chuẩn bị cho một điều tồi tệ hơn nhiều ;-) Java có thể khá tệ với nồi hơi, đôi khi.
Joachim Sauer

nhưng điều này không sáng tác: bạn không có cách nào để chuyên môn loại A mà không phải xác nhận nó thông qua một diễn viên (tôi nghĩ)
nicolas

Thật không may, điều này dường như không thể đại diện cho một số loại tổng phức tạp hơn, ví dụ Either. Xem câu hỏi của tôi
Zoey Hewll

20

Bạn có thể đạt được điều này bằng cách sử dụng mẫu khách truy cập , sẽ bổ sung khớp mẫu. Ví dụ

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

có thể được viết bằng Java như

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);
    }
}

Niêm phong được thực hiện bởi các Visitorlớp. Mỗi phương thức của nó khai báo cách giải cấu trúc một trong các lớp con. Bạn có thể thêm nhiều lớp con, nhưng nó sẽ phải thực hiện acceptvà bằng cách gọi một trong các visit...phương thức, vì vậy nó sẽ phải hành xử giống Conshoặc thích Nil.


13

Nếu bạn lạm dụng các tham số có tên C # (được giới thiệu trong C # 4.0), bạn có thể tạo các loại dữ liệu đại số dễ khớp với:

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);

Dưới đây là việc thực hiện của Eitherlớp:

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);
    }
}

Tôi đã thấy một phiên bản Java của kỹ thuật này trước đây, nhưng lambdas và các tham số được đặt tên làm cho nó rất dễ đọc. +1!
Doval

1
Tôi nghĩ vấn đề ở đây là Quyền không chung chung về loại lỗi. Một cái gì đó như : class Right<R> : Either<Bot,R>, trong đó Either được thay đổi thành giao diện với các tham số loại covariant (out) và Bot là loại dưới cùng (kiểu con của mọi loại khác, đối diện với Object). Tôi không nghĩ C # có loại dưới cùng.
croyd 24/2/2015

5

Trong C #, bạn không thể có Emptyloại đó , vì, do sự thống nhất, các loại cơ sở khác nhau đối với các loại thành viên khác nhau. Bạn chỉ có thể có Empty<T>; không hữu ích

Trong Java, bạn có thể có Empty : ConsListdo loại xóa, nhưng tôi không chắc liệu trình kiểm tra loại sẽ không hét lên ở đâu đó.

Tuy nhiên, vì cả hai ngôn ngữ đều có null, bạn có thể nghĩ tất cả các loại tham chiếu của chúng là "Dù thế nào | Không". Vì vậy, bạn chỉ cần sử dụng nullnhư "Trống" để tránh phải chỉ định những gì nó xuất phát.


Vấn đề với nullnó là quá chung chung: nó đại diện cho sự vắng mặt của bất cứ thứ gì , tức là sự trống rỗng nói chung, nhưng tôi muốn đại diện cho sự vắng mặt của các yếu tố danh sách, tức là một danh sách trống nói riêng. Một danh sách trống và một cây trống nên có các loại riêng biệt. Ngoài ra, danh sách trống cần phải là một giá trị thực tế bởi vì nó vẫn có hành vi của riêng nó, vì vậy nó cần phải có các phương thức riêng. Để xây dựng danh sách [1, 2, 3], tôi muốn nói Empty.prepend(3).prepend(2).prepend(1)(hoặc bằng ngôn ngữ với các toán tử liên kết phải 1 :: 2 :: 3 :: Empty), nhưng tôi không thể nói null.prepend ….
Jörg W Mittag

@ JörgWMittag: Các null có các loại khác nhau. Bạn cũng có thể dễ dàng tạo hằng số gõ với giá trị null cho mục đích này. Nhưng đó là sự thật bạn không thể gọi các phương thức trên nó. Cách tiếp cận của bạn với các phương thức không hoạt động mà không có phần tử cụ thể theo kiểu phần tử nào.
Jan Hudec

một số phương thức mở rộng xảo quyệt có thể giả mạo 'phương thức' gọi null (tất nhiên tất cả đều thực sự tĩnh)
jk.

Bạn có thể có một Emptyvà một Empty<>và lạm dụng khai thác chuyển đổi ngầm để cho phép một mô phỏng khá thực tế, nếu bạn muốn. Về cơ bản, bạn sử dụng Emptymã, nhưng tất cả các chữ ký loại vv chỉ sử dụng các biến thể chung.
Eamon Nerbonne

3

Điều duy nhất cần thiết ngoài phân lớp ngây thơ là một cách để đóng dấu các lớp, tức là một cách để không thể thêm các lớp con vào một hệ thống phân cấp.

Trong Java bạn không thể. Nhưng bạn có thể khai báo lớp cơ sở là gói riêng, có nghĩa là tất cả các lớp con trực tiếp phải thuộc cùng gói với lớp cơ sở. Nếu sau đó bạn khai báo các lớp con là cuối cùng, chúng không thể được phân lớp nữa.

Tôi không biết nếu điều này sẽ giải quyết vấn đề thực sự của bạn mặc dù ...


Tôi không có vấn đề thực sự, hoặc tôi đã đăng bài này lên StackOverflow, không phải ở đây :-) Một thuộc tính quan trọng của Kiểu dữ liệu đại số là chúng có thể bị đóng , có nghĩa là số lượng trường hợp đã được sửa: trong ví dụ này , một danh sách trống hoặc không. Nếu tôi có thể đảm bảo tĩnh rằng đây là trường hợp, thì tôi có thể tạo phôi động hoặc intanceofkiểm tra động "pseudo-type-safe" (nghĩa là: Tôi biết nó an toàn, ngay cả khi trình biên dịch không), chỉ cần đảm bảo rằng tôi luôn luôn kiểm tra hai trường hợp đó Tuy nhiên, nếu người khác thêm một lớp con mới, thì tôi có thể gặp lỗi thời gian chạy mà tôi không mong đợi.
Jörg W Mittag

@ JörgWMittag - Vâng Java rõ ràng không hỗ trợ điều đó ... theo nghĩa mạnh mẽ mà bạn dường như đang muốn. Tất nhiên, bạn có thể làm nhiều việc khác nhau để chặn phân nhóm không mong muốn trong thời gian chạy, nhưng sau đó bạn nhận được "lỗi thời gian chạy mà bạn không mong đợi".
Stephen C

3

Kiểu dữ liệu ConsList<A>có thể được biểu diễn dưới dạng giao diện. Giao diện hiển thị một deconstructphương thức duy nhất cho phép bạn "giải cấu trúc" một giá trị của loại đó - nghĩa là, để xử lý từng hàm tạo có thể. Các cuộc gọi đến một deconstructphương thức tương tự như một case ofhình thức trong Haskell hoặc ML.

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

Các deconstructphương pháp có một "gọi lại" chức năng cho mỗi nhà xây dựng trong ADT. Trong trường hợp của chúng tôi, nó có một hàm cho trường hợp danh sách trống và một hàm khác cho trường hợp "khuyết điểm".

Mỗi hàm gọi lại chấp nhận làm đối số các giá trị được chấp nhận bởi hàm tạo. Vì vậy, trường hợp "danh sách trống" không có đối số, nhưng trường hợp "khuyết điểm" có hai đối số: phần đầu và phần đuôi của danh sách.

Chúng ta có thể mã hóa "nhiều đối số" này bằng cách sử dụng Tuplecác lớp hoặc sử dụng currying. Trong ví dụ này, tôi đã chọn sử dụng một Pairlớp đơn giản .

Giao diện được thực hiện một lần cho mỗi nhà xây dựng. Đầu tiên, chúng tôi có việc thực hiện cho "danh sách trống". Việc deconstructthực hiện chỉ đơn giản là gọi emptyCasehàm gọi lại.

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());
  }
}

Sau đó, chúng tôi thực hiện trường hợp "khuyết điểm" tương tự. Lần này lớp có các thuộc tính: đầu và đuôi của danh sách không trống. Trong quá trình deconstructthực hiện, các thuộc tính đó được chuyển đến consCasechức năng gọi lại.

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));
  }
}

Dưới đây là một ví dụ về việc sử dụng mã hóa ADT này: chúng ta có thể viết một reducehàm là danh sách gấp thông thường.

<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))
  );
}

Điều này tương tự với việc triển khai này trong Haskell:

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

Cách tiếp cận thú vị, rất hay! Tôi có thể thấy kết nối với F # Active Forms và Scala Extractors (và có lẽ cũng có một liên kết ở đó với Haskell Views, mà tôi không biết gì về điều đó, thật không may). Tôi đã không nghĩ đến việc chuyển trách nhiệm khớp mẫu trên các hàm tạo dữ liệu vào chính thể hiện ADT.
Jörg W Mittag

2

Điều duy nhất cần thiết ngoài phân lớp ngây thơ là một cách để đóng dấu các lớp, tức là một cách để không thể thêm các lớp con vào một hệ thống phân cấp.

Làm thế nào bạn sẽ tiếp cận vấn đề này bằng một ngôn ngữ như C # hoặc Java?

Không có cách nào tốt để làm điều này, nhưng nếu bạn sẵn sàng sống với một vụ hack ghê tởm thì bạn có thể thêm một số kiểm tra loại rõ ràng vào hàm tạo của lớp cơ sở trừu tượng. Trong Java, đây sẽ là một cái gì đó như

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

Trong C #, nó phức tạp hơn vì các tổng quát thống nhất - cách tiếp cận đơn giản nhất có thể là chuyển đổi loại thành chuỗi và xâu chuỗi đó.

Lưu ý rằng trong Java thậm chí cơ chế này về mặt lý thuyết có thể được bỏ qua bởi một người thực sự muốn thông qua mô hình tuần tự hóa hoặc sun.misc.Unsafe.


1
Nó sẽ không phức tạp hơn trong C #:Type type = this.GetType(); if (type != typeof(Empty<T>) && type != typeof(ConsCell<T>)) throw new Exception();
svick

@svick, quan sát tốt. Tôi đã không tính đến loại cơ sở sẽ được tham số hóa.
Peter Taylor

Xuất sắc! Tôi đoán điều này là đủ tốt để thực hiện "kiểm tra kiểu tĩnh thủ công". Tôi đang tìm cách loại bỏ các lỗi lập trình trung thực hơn là mục đích xấu.
Jörg W Mittag
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.