คุณเข้ารหัสชนิดข้อมูลพีชคณิตในภาษา C # - หรือภาษา Java ได้อย่างไร


58

มีปัญหาบางอย่างที่แก้ไขได้อย่างง่ายดายโดย Algebraic Data Types เช่น List type สามารถแสดงออกได้อย่างชัดเจนว่า:

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 แต่มันจะคล้ายกันในภาษาอื่นที่มีการสนับสนุนเนทิฟสำหรับประเภทข้อมูลพีชคณิต

ปรากฎว่ามีการแมปที่ชัดเจนกับการพิมพ์ย่อยสไตล์ OO: ประเภทข้อมูลจะกลายเป็นคลาสฐานนามธรรมและตัวสร้างข้อมูลทั้งหมดจะกลายเป็นคลาสย่อยที่เป็นรูปธรรม นี่คือตัวอย่างใน 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 อย่างไร บล็อกที่สะดุดสองข้อที่ฉันพบเมื่อพยายามใช้ Algebraic Data Types ใน C # คือ:

  • ฉันไม่สามารถจำแนกประเภทด้านล่างที่เรียกใน C # (เช่นฉันไม่สามารถหาสิ่งที่จะใส่ลงไปclass Empty : ConsList< ??? >)
  • ฉันไม่สามารถหาวิธีปิดผนึก ConsListเพื่อไม่ให้มีการเพิ่มคลาสย่อยลงในลำดับชั้น

อะไรจะเป็นวิธีที่ใช้สำนวนที่สุดในการใช้ประเภทข้อมูลพีชคณิตใน C # และ / หรือ Java หรือถ้ามันเป็นไปไม่ได้สิ่งที่จะเปลี่ยนสำนวน?



3
C # คือภาษา OOP แก้ไขปัญหาโดยใช้ OOP อย่าลองใช้กระบวนทัศน์อื่นใด
ร่าเริง

7
@Euphoric C # ได้กลายเป็นภาษาที่ใช้งานได้ดีกับ C # 3.0 ฟังก์ชั่นชั้นหนึ่งการดำเนินงานทั่วไปที่มีอยู่ในตัว monads
Mauricio Scheffer

2
@Euphoric: บางโดเมนง่ายต่อการสร้างโมเดลด้วยออบเจ็กต์และยากต่อการสร้างโมเดลที่มีชนิดข้อมูลเชิงพีชคณิต การรู้วิธีการทำทั้งสองวิธีช่วยให้คุณมีความยืดหยุ่นในการสร้างแบบจำลองโดเมน และอย่างที่ฉันพูดการทำแผนที่ข้อมูลพีชคณิตกับแนวคิด OO ทั่วไปนั้นไม่ซับซ้อน: ประเภทข้อมูลจะกลายเป็นคลาสฐานนามธรรม (หรืออินเทอร์เฟซหรือลักษณะนามธรรม) คอนสตรัคเตอร์ข้อมูลจะกลายเป็นซับคลาสที่เป็นรูปธรรม ที่ช่วยให้คุณเปิดประเภทข้อมูลพีชคณิต ข้อ จำกัด เกี่ยวกับการสืบทอดให้ชนิดข้อมูลพีชคณิตแบบปิด ความแตกต่างให้คุณเลือกปฏิบัติกรณี
Jörg W Mittag

3
@Eparhoric กระบวนทัศน์ schmaradigm ใครสนใจ? ADTs เป็นฉากสำหรับการตั้งโปรแกรมการทำงาน (หรือ OOP หรือสิ่งอื่นใด) การเข้ารหัส AST ของภาษาใด ๆ เป็นความเจ็บปวดที่ไม่มีการสนับสนุน ADT ที่เหมาะสมและการรวบรวมภาษานั้นเป็นความเจ็บปวดที่ไม่มีคุณลักษณะกระบวนทัศน์ที่ไม่เชื่อเรื่องพระเจ้าและการจับคู่รูปแบบอื่น
SK-logic

คำตอบ:


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
อย่างใดฉันไม่แปลกใจที่เห็นชื่อของคุณปรากฏขึ้นที่นี่! ขอบคุณฉันไม่รู้จักสำนวนนี้
Jörg W Mittag

4
เมื่อคุณพูดว่า "boilerplate หนัก" ฉันถูกเตรียมไว้สำหรับบางสิ่งที่แย่กว่านั้น ;-) Java อาจไม่ดีนักในส่วนของสำเร็จรูปบางครั้ง
Joachim Sauer

แต่นี้ไม่ได้เขียน: คุณมีวิธีการมีความเชี่ยวชาญประเภทโดยไม่ต้องยืนยันผ่านหล่อ (ฉันคิด) ไม่มี
นิโคลัส

Eitherนี้น่าเสียดายที่ดูเหมือนว่าไม่สามารถที่จะเป็นตัวแทนบางชนิดรวมที่ซับซ้อนมากขึ้นเช่น ดูคำถามของฉัน
Zoey Hewll

20

คุณสามารถทำได้โดยใช้รูปแบบผู้เข้าชมซึ่งจะเสริมการจับคู่รูปแบบ ตัวอย่างเช่น

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ชั้นเรียน วิธีการแต่ละวิธีจะประกาศวิธีแยกแยะชั้นย่อยหนึ่งชั้น คุณสามารถเพิ่ม subclasses มากขึ้น แต่มันจะมีการดำเนินการacceptและโดยการเรียกหนึ่งในvisit...วิธีการดังนั้นจึงอาจจะต้องทำตัวเหมือนหรือชอบConsNil


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 ของเทคนิคนี้มาก่อน แต่ lambdas และพารามิเตอร์ที่ตั้งชื่อทำให้อ่านง่ายมาก +1!
Doval

1
ฉันคิดว่าปัญหาที่นี่คือขวาไม่ใช่ทั่วไปกับประเภทของข้อผิดพลาด สิ่งที่ต้องการ: class Right<R> : Either<Bot,R>ซึ่งทั้งสองอย่างถูกเปลี่ยนเป็นอินเทอร์เฟซที่มีพารามิเตอร์ประเภท covariant (out) และบอทเป็นประเภทด้านล่าง ฉันไม่คิดว่า C # มีประเภทด้านล่าง
croyd

5

ใน C # คุณไม่สามารถมีEmptyชนิดนั้นได้เนื่องจากเนื่องจากมีการใช้งานซ้ำชนิดฐานจะแตกต่างกันสำหรับประเภทสมาชิกที่แตกต่างกัน คุณมีได้เพียงEmpty<T>; ไม่เป็นประโยชน์

ใน Java คุณสามารถมีได้Empty : ConsListเนื่องจากการลบประเภท แต่ฉันไม่แน่ใจว่าตัวตรวจสอบประเภทจะไม่กรีดร้องอยู่ที่ไหนสักแห่ง

อย่างไรก็ตามเนื่องจากทั้งสองภาษามีnullคุณสามารถนึกถึงประเภทอ้างอิงทั้งหมดของพวกเขาว่าเป็น "Anything | Null" ดังนั้นคุณแค่ใช้คำnullว่า "Empty" เพื่อหลีกเลี่ยงการระบุสิ่งที่มันเกิดขึ้น


ปัญหาnullคือมันกว้างเกินไป: มันแสดงถึงการไม่มีอะไรเช่นความว่างเปล่าโดยทั่วไป แต่ฉันต้องการที่จะเป็นตัวแทนของการขาดองค์ประกอบรายการเช่นรายการที่ว่างเปล่าโดยเฉพาะ รายการว่างเปล่าและต้นไม้ว่างควรมีประเภทที่แตกต่างกัน นอกจากนี้รายการที่ว่างเปล่าจะต้องเป็นค่าจริงเพราะมันยังคงมีพฤติกรรมของตัวเองดังนั้นจึงจำเป็นต้องมีวิธีการของตัวเอง เพื่อสร้างรายการ[1, 2, 3]ฉันต้องการพูดEmpty.prepend(3).prepend(2).prepend(1)(หรือในภาษาที่มีตัวดำเนินการเชื่อมโยงขวา1 :: 2 :: 3 :: Empty) แต่ฉันไม่สามารถพูดnull.prepend …ได้
Jörg W Mittag

@ JörgWMittag: โมฆะมีประเภทที่แตกต่าง คุณยังสามารถสร้างค่าคงที่ที่พิมพ์ด้วยค่า null เพื่อวัตถุประสงค์ได้อย่างง่ายดาย แต่มันเป็นความจริงที่คุณไม่สามารถเรียกวิธีการต่าง ๆ ได้ วิธีการของคุณด้วยวิธีการใช้งานไม่ได้หากไม่มีองค์ประกอบเฉพาะประเภทว่าง
Jan Hudec

วิธีการส่วนขยายที่มีฝีมือบางอย่างสามารถปลอม 'method' การเรียกใช้ค่า Null (แน่นอนว่ามันคงที่ทั้งหมด)
jk

คุณสามารถมีEmptyและEmpty<>และการละเมิดแปลงนัยประกอบการที่จะช่วยให้การจำลองการปฏิบัติอย่างเป็นธรรมถ้าคุณต้องการ เป็นหลักคุณใช้Emptyในรหัส แต่ลายเซ็นประเภทอื่น ๆ ทั้งหมดใช้ตัวแปรทั่วไปเท่านั้น
Eamon Nerbonne

3

สิ่งเดียวที่จำเป็นนอกเหนือจากคลาสย่อยที่ไร้เดียงสาคือวิธีการปิดคลาสเช่นวิธีทำให้เป็นไปไม่ได้ที่จะเพิ่มคลาสย่อยให้กับลำดับชั้น

ใน Java คุณไม่สามารถ แต่คุณสามารถประกาศคลาสพื้นฐานเป็นแพ็กเกจส่วนตัวซึ่งหมายความว่าคลาสย่อยโดยตรงทั้งหมดต้องเป็นของแพ็คเกจเดียวกันกับคลาสพื้นฐาน หากคุณประกาศคลาสย่อยเป็นครั้งสุดท้ายพวกเขาจะไม่สามารถแบ่งคลาสย่อยได้อีก

ฉันไม่รู้ว่าจะแก้ปัญหาที่แท้จริงของคุณได้หรือไม่ ...


ฉันไม่มีปัญหาจริงหรือฉันจะโพสต์สิ่งนี้ใน StackOverflow ไม่ใช่ที่นี่ :-) คุณสมบัติที่สำคัญของประเภทข้อมูลพีชคณิตคือพวกเขาสามารถปิดได้ซึ่งหมายความว่าจำนวนผู้ป่วยได้รับการแก้ไข: ในตัวอย่างนี้ รายการว่างเปล่าหรือไม่ หากฉันสามารถมั่นใจได้ว่าเป็นกรณีนี้ฉันจะสร้าง casts แบบไดนามิกหรือการintanceofตรวจสอบแบบไดนามิก"pseudo-type-safe" (เช่น: ฉันรู้ว่ามันปลอดภัยแม้ว่าคอมไพเลอร์จะไม่) เพียงแค่ทำให้แน่ใจว่าฉันเสมอ ตรวจสอบทั้งสองกรณี อย่างไรก็ตามหากมีคนอื่นเพิ่มคลาสย่อยใหม่จากนั้นฉันจะได้รับข้อผิดพลาด runtime ที่ฉันไม่ได้คาดหวัง
Jörg W Mittag

@ JörgWMittag - Java อย่างชัดเจนไม่สนับสนุนที่ ... ในแง่ดีที่คุณดูเหมือนจะต้องการ แน่นอนคุณสามารถทำสิ่งต่าง ๆ เพื่อป้องกัน subtyping ที่ไม่ต้องการขณะใช้งาน แต่คุณจะได้รับ "ข้อผิดพลาด runtime ที่คุณไม่คาดคิด"
สตีเฟ่นซี

3

ประเภทข้อมูลConsList<A>สามารถแสดงเป็นอินเทอร์เฟซ อินเทอร์เฟซเปิดเผยdeconstructวิธีการเดียวซึ่งช่วยให้คุณ "deconstruct" ค่าของประเภทนั้น - นั่นคือการจัดการแต่ละตัวสร้างที่เป็นไปได้ การเรียกใช้deconstructเมธอดนั้นคล้ายคลึงกับcase ofฟอร์มใน Haskell หรือ ML

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

deconstructวิธีการจะมีการ "โทรกลับ" ฟังก์ชั่นสำหรับแต่ละตัวสร้างใน ADT ในกรณีของเรามันใช้ฟังก์ชั่นสำหรับกรณีรายการว่างเปล่าและฟังก์ชั่นอื่นสำหรับกรณี "cons เซลล์"

แต่ละฟังก์ชั่นการโทรกลับยอมรับเป็นอาร์กิวเมนต์ค่าที่ได้รับการยอมรับจากตัวสร้าง ดังนั้นกรณี "รายการที่ว่างเปล่า" ไม่มีข้อโต้แย้งใด ๆ แต่กรณี "ข้อเสียของเซลล์" ใช้สองข้อโต้แย้งคือส่วนหัวและส่วนท้ายของรายการ

เราสามารถเข้ารหัส "หลายอาร์กิวเมนต์" เหล่านี้โดยใช้Tupleคลาสหรือใช้การแก้เผ็ด ในตัวอย่างนี้ฉันเลือกที่จะใช้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());
  }
}

จากนั้นเราจะใช้กรณี "เซลล์เสีย" ในทำนองเดียวกัน เวลานี้คลาสมีคุณสมบัติ: ส่วนหัวและส่วนท้ายของรายการที่ไม่ว่างเปล่า ในการ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 # Active Patterns และ Scala Extractors (และอาจมีลิงค์ไปยัง Haskell Views ด้วยซึ่งน่าเสียดายที่ฉันไม่รู้อะไรเลย) ฉันไม่คิดว่าจะย้ายความรับผิดชอบในการจับคู่รูปแบบไปยังตัวสร้างข้อมูลในอินสแตนซ์ ADT เอง
Jörg W Mittag

2

สิ่งเดียวที่จำเป็นนอกเหนือจากคลาสย่อยที่ไร้เดียงสาคือวิธีการปิดคลาสเช่นวิธีทำให้เป็นไปไม่ได้ที่จะเพิ่มคลาสย่อยให้กับลำดับชั้น

คุณจะแก้ไขปัญหานี้ด้วยภาษาเช่น C # หรือ Java อย่างไร

ไม่มีวิธีที่ดีในการทำเช่นนี้ แต่ถ้าคุณยินดีที่จะอยู่กับแฮ็คที่น่าเกลียดคุณสามารถเพิ่มการตรวจสอบประเภทที่ชัดเจนลงในตัวสร้างคลาสฐานนามธรรม ในชวาสิ่งนี้จะเป็นเช่นนั้น

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

ใน C # มีความซับซ้อนมากขึ้นเนื่องจากข้อมูลทั่วไป reified - วิธีที่ง่ายที่สุดอาจจะแปลงชนิดเป็นสตริงและ mangle ที่

โปรดสังเกตว่าใน Java sun.misc.Unsafeแม้กลไกนี้ในทางทฤษฎีสามารถข้ามโดยคนที่ต้องการจริงๆที่จะผ่านรูปแบบอนุกรมหรือ


1
มันจะไม่ซับซ้อนมากขึ้นใน C #:Type type = this.GetType(); if (type != typeof(Empty<T>) && type != typeof(ConsCell<T>)) throw new Exception();
svick

@Svick สังเกตได้ดี ฉันไม่ได้คำนึงถึงว่าประเภทฐานจะเป็นพารามิเตอร์
Peter Taylor

ยอดเยี่ยม! ฉันเดาว่านี่ดีพอสำหรับการทำ "การตรวจสอบชนิดคงที่ด้วยตนเอง" ฉันกำลังมองหาที่จะกำจัดข้อผิดพลาดการเขียนโปรแกรมที่ซื่อสัตย์มากกว่าเจตนาที่เป็นอันตราย
Jörg W Mittag
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.