आप सी # - या जावा जैसी भाषा में बीजीय डेटा प्रकार को कैसे एन्कोड करते हैं?


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

यह विशेष उदाहरण हास्केल में है, लेकिन यह अन्य भाषाओं में बीजगणितीय डेटा प्रकारों के लिए देशी समर्थन के समान होगा।

यह पता चलता है कि OO- शैली के उप-योग के लिए एक स्पष्ट मानचित्रण है: डेटाटैब एक अमूर्त आधार वर्ग बन जाता है और हर डेटा कंस्ट्रक्टर एक ठोस उपवर्ग बन जाता है। यहाँ एक उदाहरण है स्काला में:

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 # में क्या कहा जाता है (यानी मैं यह पता नहीं लगा सकता कि क्या डालूं class Empty : ConsList< ??? >)
  • मैं सील ConsList करने का कोई तरीका नहीं निकाल सका ताकि कोई उपवर्ग पदानुक्रम में न जोड़ा जा सके

सी # और / या जावा में बीजगणितीय डेटा प्रकारों को लागू करने का सबसे मुहावरेदार तरीका क्या होगा? या, यदि यह संभव नहीं है, तो मुहावरेदार प्रतिस्थापन क्या होगा?



3
C # OOP भाषा है। OOP का उपयोग कर समस्याओं का समाधान। किसी भी अन्य प्रतिमान का उपयोग करने की कोशिश मत करो।
व्यंग्य

7
@ यूफ़ोरिक सी # सी # 3.0 के साथ एक बहुत ही उपयोगी कार्यात्मक भाषा बन गई है। प्रथम श्रेणी के कार्य, बिल्ट-इन कॉमन फंक्शनल ऑपरेशंस, मोनड्स।
मौरिसियो शेफ़र

2
@ यूफोरिक: कुछ डोमेन ऑब्जेक्ट्स के साथ मॉडल करना आसान है और बीजीय डेटा प्रकारों के साथ मॉडल करना मुश्किल है, कुछ इसके विपरीत हैं। यह जानने के लिए कि दोनों को कैसे किया जाता है, आपको अपने डोमेन के मॉडलिंग में अधिक लचीलापन देता है। और जैसा कि मैंने कहा, बीजीय डेटा प्रकारों को विशिष्ट ओओ अवधारणाओं के लिए मैप करना उतना जटिल नहीं है: डेटा प्रकार एक एब्सट्रैक्ट बेस क्लास (या एक इंटरफ़ेस, या एक अमूर्त विशेषता) बन जाता है, डेटा कंस्ट्रक्टर कंक्रीट कार्यान्वयन उपवर्ग बन जाते हैं। यह आपको एक खुला बीजीय डेटा प्रकार प्रदान करता है। वंशानुक्रम पर प्रतिबंध आपको एक बंद बीजीय डेटा प्रकार देता है। बहुरूपता आपको मामले में भेदभाव देता है।
Jörg W Mittag

3
@ कामुक, प्रतिमान, schmaradigm, कौन परवाह करता है? ADT कार्यात्मक प्रोग्रामिंग (या OOP या जो कुछ भी है) के लिए रूढ़िवादी हैं। किसी भी भाषा के एएसटी को एनकोड करना एक अच्छा दर्द है बिना एडीटी के समर्थन के बिना, और उस भाषा को संकलित करना एक अन्य प्रतिमान-अज्ञेयवादी विशेषता, पैटर्न मिलान के बिना एक दर्द है।
एसके-तर्क

जवाबों:


42

जावा में कक्षाओं को सील करने का एक आसान, लेकिन बॉयलरप्लेट है। आप एक निजी कंस्ट्रक्टर को बेस क्लास में रखते हैं, फिर उसके उप-वर्गों को बनाते हैं।

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: जावा बीजीय डेटाटाइप्स आपके लिए वो सभी बॉयलरप्लेट तैयार करती है https://github.com/JamesIry/jADT


2
किसी तरह मैं आपका नाम यहाँ देखकर हैरान नहीं हूँ! धन्यवाद, मैं इस मुहावरे को नहीं जानता था।
जोर्ग डब्ल्यू मित्तग

4
जब आपने कहा "बॉयलरप्लेट हैवी" तो मैं कुछ ज्यादा ही खराब हो गया था ;-) बॉयलर, कभी-कभी जावा बहुत खराब हो सकता है।
जोकिम सॉर

लेकिन यह रचना नहीं करता है: आपके पास टाइप ए का विशेषज्ञ होने का कोई रास्ता नहीं है, क्योंकि यह एक डाली (मुझे लगता है) के माध्यम से जोर देने के लिए है
निकोलस

यह दुर्भाग्य से कुछ और जटिल योग प्रकारों का प्रतिनिधित्व करने में असमर्थ है, जैसे Eitherमेरा प्रश्न
Zoey Hewll

20

आप विज़िटर पैटर्न का उपयोग करके इसे प्राप्त कर सकते हैं , जो पैटर्न मिलान का पूरक होगा। उदाहरण के लिए

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

के रूप में जावा में लिखा जा सकता है

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या पसंद करना होगा Nil


13

यदि आप सी # नामित मापदंडों का दुरुपयोग करते हैं (सी # 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);
    }
}

मैंने इस तकनीक का जावा संस्करण पहले देखा है, लेकिन लैम्ब्डा और नामित पैरामीटर इसे बहुत पठनीय बनाते हैं। +1!
डोभाल

1
मुझे लगता है कि समस्या यह है कि राइट त्रुटि के प्रकार पर सामान्य नहीं है। कुछ इस तरह से: class Right<R> : Either<Bot,R>जहां या तो सहसंयोजक (आउट) प्रकार के मापदंडों के साथ एक इंटरफ़ेस में बदल दिया जाता है, और बॉट नीचे प्रकार (ऑब्जेक्ट के विपरीत, प्रत्येक अन्य प्रकार का उपप्रकार) है। मुझे नहीं लगता कि C # में एक निचला प्रकार है।
क्रोइड

5

C # में, आपके पास वह Emptyप्रकार नहीं हो सकता है , क्योंकि, संशोधन के कारण, आधार प्रकार विभिन्न सदस्य प्रकारों के लिए भिन्न होते हैं। तुम ही हो सकते हो Empty<T>; यह उपयोगी नहीं है।

जावा में, आप Empty : ConsListटाइप इरिटेशन के कारण हो सकते हैं , लेकिन मुझे यकीन नहीं है कि टाइप चेकर कहीं चिल्लाएगा नहीं।

हालाँकि null, दोनों भाषाओं में , आप उनके सभी संदर्भ प्रकारों के बारे में सोच सकते हैं जैसे कि "जो भी | अशक्त"। तो आप केवल null"खाली" के रूप में इसका उपयोग करने से बचने के लिए उपयोग करेंगे जो यह बताता है कि यह क्या है।


इसके साथ समस्या nullयह है कि यह बहुत सामान्य है: यह किसी भी चीज की अनुपस्थिति का प्रतिनिधित्व करता है , अर्थात सामान्य रूप से शून्यता , लेकिन मैं सूची तत्वों की अनुपस्थिति का प्रतिनिधित्व करना चाहता हूं, विशेष रूप से एक खाली सूची। एक खाली सूची और एक खाली पेड़ के अलग-अलग प्रकार होने चाहिए। साथ ही, खाली सूची का वास्तविक मूल्य होना आवश्यक है क्योंकि इसमें अभी भी उसका स्वयं का व्यवहार है, इसलिए उसे स्वयं के तरीकों की आवश्यकता है। सूची का निर्माण करने के लिए [1, 2, 3], मैं कहना चाहता हूं Empty.prepend(3).prepend(2).prepend(1)(या दाएं-सहयोगी ऑपरेटरों के साथ एक भाषा में 1 :: 2 :: 3 :: Empty), लेकिन मैं नहीं कह सकता null.prepend …
जोर्ज डब्ल्यू मित्तग

@ JörgWMittag: नल अलग-अलग प्रकार के होते हैं। आप उद्देश्य के लिए मूल्य नल के साथ आसानी से टाइप किए गए स्थिर बना सकते हैं। लेकिन यह सच है कि आप इस पर तरीके नहीं कह सकते। विधियों के साथ आपका दृष्टिकोण वैसे भी तत्व-प्रकार-विशिष्ट खाली के बिना काम नहीं करता है।
Jan Hudec

कुछ चालाक विस्तार विधियाँ nulls (निश्चित रूप से इसके सभी वास्तव में स्थिर) पर नकली 'विधि' कॉल कर सकते हैं
jk।

यदि आप चाहें, तो आप एक Emptyऔर एक Empty<>और दुरुपयोग निहित रूपांतरण ऑपरेटरों को एक व्यावहारिक रूप से व्यावहारिक सिमुलेशन की अनुमति दे सकते हैं। अनिवार्य रूप से, आप Emptyकोड में उपयोग करते हैं , लेकिन सभी प्रकार के हस्ताक्षर आदि केवल सामान्य वेरिएंट का उपयोग करते हैं।
ईमोन नेरबोन

3

भोले उपवर्ग से परे केवल एक चीज की आवश्यकता है, वर्गों को सील करने का एक तरीका, यानी एक पदानुक्रम में उपवर्गों को जोड़ना असंभव बनाना।

जावा में आप नहीं कर सकते। लेकिन आप आधार वर्ग को पैकेज निजी घोषित कर सकते हैं, जिसका अर्थ है कि सभी प्रत्यक्ष उपवर्गों को आधार वर्ग के समान पैकेज से संबंधित होना चाहिए। यदि आप उपवर्गों को अंतिम घोषित करते हैं, तो उन्हें किसी भी तरह से उपवर्गित नहीं किया जा सकता है।

मैं नहीं जानता कि क्या यह आपकी वास्तविक समस्या का समाधान करेगा ...


मुझे कोई वास्तविक समस्या नहीं है, या मैंने इसे StackOverflow पर पोस्ट किया होगा, यहाँ नहीं :-) बीजगणितीय डेटा प्रकारों की एक महत्वपूर्ण संपत्ति यह है कि उन्हें बंद किया जा सकता है , जिसका अर्थ है कि मामलों की संख्या तय है: इस उदाहरण में , एक सूची या तो खाली है या यह नहीं है। अगर मैं स्टेटिकली यह सुनिश्चित कर सकता हूं कि यह मामला है, तो मैं डायनेमिक कास्ट या डायनेमिक intanceofचेक "स्यूडो-टाइप-सेफ" बना सकता हूं (यानी: मुझे पता है कि यह सुरक्षित है, भले ही कंपाइलर न हो), बस यह सुनिश्चित करके कि मैं हमेशा उन दो मामलों की जाँच करें। यदि, हालांकि, कोई अन्य व्यक्ति एक नया उपवर्ग जोड़ता है, तो मुझे रनटाइम त्रुटियां मिल सकती हैं, जिसकी मुझे उम्मीद नहीं थी।
Jörg W Mittag

@ JörgWMittag - वैसे जावा स्पष्ट रूप से उस समर्थन का समर्थन नहीं करता है ... जिस मजबूत अर्थ में आप चाहते हैं। बेशक, आप रनटाइम पर अवांछित सबटाइपिंग को ब्लॉक करने के लिए विभिन्न चीजें कर सकते हैं, लेकिन फिर आपको "रनटाइम त्रुटियां मिलती हैं जिनकी आपको उम्मीद नहीं है"।
स्टीफन सी

3

डेटा प्रकार ConsList<A>को एक इंटरफ़ेस के रूप में दर्शाया जा सकता है। इंटरफ़ेस एक एकल deconstructविधि को उजागर करता है जो आपको उस प्रकार के एक मूल्य को "डिकॉन्स्ट्रक्ट" करने की अनुमति देता है - वह यह है कि प्रत्येक संभावित एक्टर्स को संभालना है। deconstructविधि के लिए कॉल case ofहास्केल या एमएल में एक रूप के अनुरूप हैं ।

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

deconstructविधि एडीटी में प्रत्येक निर्माता के लिए एक "कॉलबैक" समारोह लेता है। हमारे मामले में, यह खाली सूची मामले के लिए एक फ़ंक्शन लेता है, और "कॉन्स सेल" मामले के लिए एक और फ़ंक्शन।

प्रत्येक कॉलबैक फ़ंक्शन उन मानों को तर्क के रूप में स्वीकार करता है, जो निर्माता द्वारा स्वीकार किए जाते हैं। तो "खाली सूची" मामले में कोई तर्क नहीं है, लेकिन "विपक्ष सेल" मामले में दो तर्क हैं: सूची का प्रमुख और पूंछ।

हम इन "कई तर्कों" को 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));
  }
}

यहां ADTs के इस एन्कोडिंग का उपयोग करने का एक उदाहरण है: हम एक 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))
  );
}

यह हास्केल में इस कार्यान्वयन के अनुरूप है:

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

दिलचस्प दृष्टिकोण, बहुत अच्छा! मैं एफ # सक्रिय पैटर्न और स्काला एक्सट्रैक्टर्स के लिए कनेक्शन देख सकता हूं (और संभवत: वहां हास्केल व्यू के लिए एक लिंक भी है, जिसके बारे में मुझे कुछ भी नहीं पता है, दुर्भाग्य से)। मैंने एडीटी उदाहरण में डेटा कंस्ट्रक्टर्स पर मेल खाने वाले पैटर्न के लिए जिम्मेदारी को आगे बढ़ाने के बारे में नहीं सोचा था।
जोर्ग डब्ल्यू मित्तग

2

भोले उपवर्ग से परे केवल एक चीज की आवश्यकता है, वर्गों को सील करने का एक तरीका, यानी एक पदानुक्रम में उपवर्गों को जोड़ना असंभव बनाना।

आप इस समस्या को C # या Java जैसी भाषा में कैसे समझेंगे?

ऐसा करने का एक अच्छा तरीका नहीं है, लेकिन यदि आप एक छिपी हुई हैक के साथ रहने के लिए तैयार हैं, तो आप कुछ स्पष्ट प्रकार की जाँच कर सकते हैं सार बेस क्लास 'कंस्ट्रक्टर। जावा में, यह कुछ इस तरह होगा

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

C # में यह संशोधित जेनरिक की वजह से अधिक जटिल है - सबसे सरल तरीका यह हो सकता है कि टाइप को स्ट्रिंग में परिवर्तित कर दिया जाए और मैनगल किया जाए।

ध्यान दें कि जावा में भी यह तंत्र सैद्धांतिक रूप से किसी ऐसे व्यक्ति द्वारा बाईपास किया जा सकता है जो वास्तव में क्रमांकन मॉडल के माध्यम से या चाहता है sun.misc.Unsafe


1
यह अधिक जटिल नहीं होगा C #:Type type = this.GetType(); if (type != typeof(Empty<T>) && type != typeof(ConsCell<T>)) throw new Exception();
svick

@svick, अच्छी तरह से मनाया। मैं इस बात पर ध्यान नहीं दे रहा था कि आधार प्रकार का मानकीकरण किया जाएगा।
पीटर टेलर

प्रतिभाशाली! मुझे लगता है कि "मैनुअल स्टैटिक टाइप चेकिंग" करने के लिए यह काफी अच्छा है। मैं दुर्भावनापूर्ण इरादे के बजाय ईमानदार प्रोग्रामिंग त्रुटियों को खत्म करना चाहता हूं।
जोर्ज डब्ल्यू मित्तग
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.