"to MI or not to MI"



  • So wie ich Xin verstanden habe, wandelt er

    struct Mensch {
      Kopf kopf;
      Bein links, Bein rechts;
    };
    

    in

    struct LinkesBein : Bein {};
    struct RechtesBein : Bein {};
    struct Mensch : Kopf, LinkesBein, RechtesBein {};
    

    um.

    Sein Argument war, dass er dann statt mensch.kopf.nicke() einfach mensch.nicke() schreiben kann, im Falle des Diamantproblems halt mensch.LinkesBein::schreite() .

    Aus klassischer OOP-Sicht ist das natürlich kreuzfalsch und das kann man ihm mit dem klassischen OOP-Vokabular problemlos beweisen. Deshalb sollte man hier auch nicht von Vererbung sprechen.

    Allerdings sehe ich noch nicht ganz den Mehrwert, eher die Probleme, die auftreten. Wenn man dem Menschen noch einen weiteren Kopf hinzufügt, kompiliert Code nicht mehr, der früher problemlos ging; was heisst, dass hier die Kapselung sehr niedrig ist.



  • Thingy schrieb:

    So wie ich Xin verstanden habe, wandelt er

    struct Mensch {
      Kopf kopf;
      Bein links, Bein rechts;
    };
    

    in

    struct LinkesBein : Bein {};
    struct RechtesBein : Bein {};
    struct Mensch : Kopf, LinkesBein, RechtesBein {};
    

    um.

    Sein Argument war, dass er dann statt mensch.kopf.nicke() einfach mensch.nicke() schreiben kann, im Falle des Diamantproblems halt mensch.LinkesBein::schreite() .

    Aus klassischer OOP-Sicht ist das natürlich kreuzfalsch und das kann man ihm mit dem klassischen OOP-Vokabular problemlos beweisen. Deshalb sollte man hier auch nicht von Vererbung sprechen.

    Das wäre auch kreuzgefährlich.
    Jemand hat vergessen zu schreiben

    Mensch::umdrehen(int anzahlViertelDeheungenNachRechts){
       for(int i=0;i<anzahlViertelDeheungenNachRechts;++i){
          RechtsBein::schrittZurück();
          LinkesBein::SchrittVor();
          Hüfte::ausrichten();
          RechtsBein::halbRanziehen();
          LinkesBein::ranziehen();
       }
    }
    

    Kommt ja mal vor, daß man was vergessen hat.
    Es folgt

    Mensch::onHoereHupeVonHinten(){
       umdrehen(4);//STILLSCHWEIGEND GEERBT VON KOPF!!!!!!
       ...
    }
    

    Und zack, Kopf umgedreht, Genickbruch, tot.
    Das ist nicht optimal.

    Es ist nicht gut, statt statt mensch.kopf.nicke() einfach mensch.nicke() schreiben können zu wollen.
    🤡



  • Kellerautomat schrieb:

    Xin schrieb:

    ...mag mich aber auch keiner Diskussion entziehen, die offen und konstruktiv werden soll. Ich hoffe heute abend nochmal Zeit zu finden und klinke mich dann nochmal ein.

    😞

    Sorry. Ich bin an dem Abend nicht mehr zu Hause angekommen, der Freitag und Samstag waren vorher schon ausgebucht und dann kommt erstmal mein eigenes Forum und jede Menge anderer Kram und "schon" bin ich hier und darf mich erstmal einlesen. 🙂

    Notfalls PM - ich kann ja auch mal was vergessen.

    Ich weiß auch nicht, ob ich jetzt auf alles eingehe - das Posting wird vermutlich eh elendig lang.

    Trotzdem eine kurze allgemeine Einleitung: Kellerautomat bemerkte ja bereits, dass viele Ideen über "Vererbung" ausgedrückt werden. Wenn also "vererbt" wird, dann heißt das nicht zwangsläufig, dass hier irgendwas etwas an eine nachfolgende Generation von Datentypen übergeben wird.

    Punkte, die Biegungen beschreiben, erben von einfachen Punkt. Alles geht als Member, aber ein Biegepunkt ist auch ein Punkt. Hier wird etwas vererbt. Das ganze dient dazu, Objekte zu klassifizieren: Eben Klassen zu bauen. Der Biegepunkt gehört zur Klasse der Punkte.

    Über virtuelle Funktionen kann man nun erlauben, Methoden auf den spezielleren Typ anzupassen. Ab hier befinden wir uns in der Objektorientierten Programmierung, bzw. besser gesagt in der "Datentyporientierten Programmierung", weil das Objekt ist egal, die Funktion wird anhand ja des Datentyps des Objekts bestimmt, aber der Spaß ist als OOP bekannt.
    Vorher haben wir einfach nur klassifiziert. Vererbung drückt Subklassifizierungen aus.

    "Erbt" man von einem Interface, so hat das eigentlich nichts mit "man bekommt etwas" zu tun, sondern man muss etwas tun: nämlich die Pure Virtual Methods implementieren. Hier wird über die "Vererbung" eher eine Vertrag ausgedrückt, ähnlich wie mit Referenzen der Vertrag zustande kommt, dass das referenzierte Objekt nicht Null ist oder bei const der Vertrag vermittelt wird, dass das Objekt eben nicht verändert wird.

    class VarDecl : public SubTypeableOperator
                  , public XSD::String::VirtualConstIdentifierInterface
    

    Die Syntax ist ähnlich, aber es passiert eigentlich etwas vollkommen anderes. Ich behaupte, dass eine Variablendeklaration ein SubtypeableOperator ist und gehe gleichzeitig den Vertrag ein, dass ich diese Klasse nach einem Identifier fragen darf. An der Klassifizierung der Klasse ändert die zusätzliche Möglichkeit eigentlich nichts. Man kann jetzt natürlich sagen, dass VarDecl zur Klasse der Identifierzurückgeber gehört, aber dafür hat sie weder Member noch Fähigkeiten gerbt, ich drücke hier nur aus, dass VarDecl selbst etwas tun muss.

    Wichtig hier ist schonmal, dass wir bisher zwei unterschiedliche Ideen, zwei unterschiedliche Ansprüche mit dem Wort "Vererbung" bezeichnen. Diese zwei sind derzeit üblicherweise anerkannt.

    Kellerautomat schrieb:

    Einerseitz finde ich Xins Ansatz interessant, rein logisch kann ich dem jedoch (noch) nichts abgewinnen. Darum moechte ich Xin bitten, ein nicht konstruiertes Modell aus realem Code zu zeigen und zu erklaeren. Beispiele tendieren dazu, als Diskussionsgrundlage ungeeignet zu sein. Vielleicht lerne ich ja etwas dazu - bisher habe ich MI kaum verwendet.

    Ich schreibe ja eine Programmiersprache. Nun gibt es in Programmiersprachen Beispielsweise Klammern - "Scopes". Innerhalb dieser Scopes darf man neue Typen definieren, so darf man innerhalb eines Scopes eine neue struct definieren, aber eben

    int main() 
    { 
      {
        struct Point { int x; int y; }; 
      }
    
      // Datentyp Point unbekannt.
    }
    

    Diese Scopes sind Operatoren, die eigene Typdefinitionen enthalten dürfen. Die Fähigkeit eine Liste von Datentypen zu zu kennen heißt bei mir SubTypeable und ein Operator ist halt ein Operator. "Scopes" erbt also von der Klasse "SubtypeableOperator", weil er als Operator in den Abstract Syntax Tree eingeht und man eben Typen hinzufügen kann.

    class SubTypeableOperator : public Operator
                              , public SubTypeable
    

    Das dürfte etwa das sein, wobei einige hier Probleme sehen. Warum auch immer. Da sich weder Operator noch Subtypeable in die Quere kommen, besteht für mich überhaupt kein Grund, Subtypeable als Member zu nehmen. Zumal hier die Frage nach der Klassifizierung zu bejahen ist: Ein SubTypeable-Opertor ist fähig Typdefinitionen aufzunehmen und es ist ein Operator. Es werden auch nicht nur "Verträge" geschlossen, sondern es werden tatsächlich Fähigkeiten vererbt.

    Nächster Schritt: Ein Quelltext ist bei mir ein Operator.
    Klingt erstmal merkwürdig, ist in meinem Sprachdesign aber so gewünscht.

    class SourceOperator : public Source
                         , public SubTypeableOperator
    

    Ein Quelltext wird erstmal geladen. Die Quelle hat also gewisse Grundeigenschaften: Zum Beispiel einen Namen:

    class Source : public DoubleCodePosition
                 , public XSD::Type::CNamedNode<Source>
                 , public OpPrio
    

    Bei DoubleCodePosition werden einige wieder auf die Barrikaden gehen, das könnte man auch als Member bezeichnen, aber wenn ich eine Fehlermeldung ausgebe, dann will ich einfach nur die Codeposition wissen und werfe die Variable, die den Quelltext repräsentiert direkt in den Logger. Für mich praktisch - und auch nachvollziehbar, denn in dem Quelltext ist ein Fehler, und der Logger holt sich direkt die Stelle im Code raus, wo ich den Fehler entdeckt habe. Warum soll ich mich darum kümmern?

    Was ist das jetzt für eine Beziehung? Es wird etwas geerbt, aber es ist keine "ist-ein" Beziehung. Es ist der Vertrag, dass man das Objekt fragen kann, wo es gerade dran ist. Würde ich ein CodePositionInterface vererben, das von mir verlangt, ein GetCurrentCodePosition() und ein SetCurrentCodePosition() zu implementieren und damit den entsprechenden Member zu bearbeiten, wäre das allseits gute und anerkannte Programmierung. Jetzt spare ich mir den Blödsinn und leite direkt davon ab, muss keine pure virtual Funktionen implementieren und keine Member erzeugen, um die Funktionen richtig bedienen zu können, ich muss mich um gar nichts kümmern, nach der Ableitung funktioniert das schon alles.
    Das ist das hierzulande falsch, obwohl hier nichts anderes passiert - wie ich vorher schon sagte: Aggregation, Komposition, Mehrfachvererbung... am Schluss kommt das gleiche raus, die Frage ist, welchen Aufwand man selbst damit hat.
    Die Frage mit den leeren Strukturen hat sich mir noch nie bei Vererbung gestellt, außer als Marker bei Templates brauchte ich noch nie leere Strukturen.

    Source selbst erbt weiterhin einen Namen - in der Regel den Dateinamen - und die Fähigkeit Operatorprioritäten zu bestimmen. Das Zeug brauche ich, um Quelltext zu lesen.

    Weiterhin ist ein SourceOperator ein SubTypeableOperator. Während ich also den Quelltext lese, baue ich die entsprechenden Dateitypen und Funktionen auf und lege sie im SourceOperator ab, der Teil des AST wird. Ein Sourceoperator ist ein Subtypeable-Operator, der fähig ist, einen AST aufzubauen.

    In C++ kommt das am ehesten #include.
    Hier werden Fähigkeiten zusammengesetzt. Von Operator "erbt" man eine virtuelle Funktion in der der SourceOperator beschreiben muss, was während des Parsens zu tun gedenkt. Das ist der Klebstoff, der Source und Subtypeable-Operator zusammenfügt.

    knivil schrieb:

    Erklaer mal warum Vererbung anstatt Aggregation benutzt werden sollte.

    Interessanter Anspruch. Wenn man sich in der Geschichte der Programmiersprachen so umschaut, ist die Formulierung mit Membern älter als die Vererbung. Strukturen/Records gibt es schon recht lange, Vererbung hingegen erst später.
    Üblicherweise lernt man Strukturen auch deutlich vor Vererbung, was schon alleine deswegen logisch und notwendig ist, weil man bekanntlich nicht von Primitiven ableiten kann. Das ist schon ein guter Grund, warum bei Primitiven keine Ableitung verwendet werden sollte.

    Nehmen wir eine Adresse:

    class Address
    {
      protected :
        unsigned int id;
      public:
        string       street;
        unsigned int housenumber;
        unsigned int zipcode;
        string       town;
    };
    
    printZipCopde( unsigned int zip );
    

    In die Funktion printZipCode kann ich alles reinwerfen, was sich als unsigned int verkaufen lässt, zum Beispiel die Hausnummer. Würde ich aber die Struktur mit Ableitungen versehen, habe ich die passenden Typen und kann die auch verlangen:

    class Address
      : protected Id
      , public    Street
      , public    HouseNumber
      , public    ZipCode
      , public    Town
    {};
    
    printZipCopde( ZipCode zip );
    

    Ich habe mal eine protected Id eingefügt, damit man sieht, dass wir hier auch nicht an der Sichtbarkeit scheitern.

    Für die Ableitung muss ich Typen generieren. Auf Deine Frage, warum man Vererbung nutzen sollte, kann ich Dir keine Antwort geben, da ich ja behaupte, das prinzipiell eh das gleiche rauskommt. Schau Dir OOP in C an. Da wird über Member vererbt, weil man keine Ableitungen formulieren kann.
    Ich kann Dich aber fragen, warum man eigentlich Membervariablen verwenden sollte.

    Man muss mal ganz klar sehen und auch sagen, dass ich mich gedanklich nicht in den Grenzen von C++ bewege, aber hier mit C++ Gedanken formuliere. Ich bin aber nicht auf die Syntax von C++ angewiesen.
    Ich arbeite hart daran, aus den Grenzen auszubrechen, die ich genauso selbstverständlich sehe, wie jeder, der ein paar Jahre programmiert. Man kann nicht von int ableiten. Warum nicht? Ich habe gedanklich überhaupt keine Skrupel von int abzuleiten, ich kann es in C++ nur nicht ausdrücken. Ich muss erst eine struct Integer mit einem int als Member bauen, dann geht's.

    struct ZipCode
      : public Integer
    {};
    

    Das Problem ist also nicht, dass es nicht machbar wäre, sondern das es in C++ nicht so einfach formulierbar ist. C++ ist dafür nicht optimal ausgelegt, was ich ganz klar darauf schiebe, dass C++ eine Weiterentwicklung von C ist, entsprechend aufgebaut auf die Probleme und Möglichkeiten, die man zu der Zeit hatte. Vererbung und auch OOP war in C nicht unbekannt, aber eben bei weitem nicht so üblich, wie es das heute ist. Also hat man erstmal einfach genauso auf wieder die Member gesetzt, wie zuvor in C, in Algol oder Fortran und die Vererbung hinzugefügt.

    Shade Of Mine schrieb:

    Denn alles was dir eine Ableitung bringt, ist die schreibarbeit fuer data() und op[] zu ersparen.

    Da hast Du in C++ grundsätzlich recht. Aber erstens ist das Einsparen von Schreibarbeit in meinen Augen besser als Schreibarbeit und zum anderen spart es auch "Lesarbeit", da die Memberbeschreibung, sofern sie eindeutig ist, fehlt, wie oben beim Logger: Ich will nicht wissen, dass der Logger die aktuelle Zeilennummer des Quelltextes braucht, mir reicht es die Variable source reinzupacken, statt die Zeile mit source.getCodePosition() voll zu müllen und die zu loggende Meldung rauszuschieben.

    Shade Of Mine schrieb:

    Schreibarbeit sparen soll dir deine IDE, nicht Vererbung.

    Ich will mich möglichst kurz ausdrücken können und ich will möglichst wenig lesen müssen. Und ich möchte möglichst auf Membernamen verzichten, weil sich Fähigkeiten nicht in den Membernamen widerspiegeln, sondern in den Typen und deren Aufbau.

    SideWinder schrieb:

    Und in diesem Zusammenhang hab ich auch ein gutes Beispiel für MI:
    Node - ChildNode (hat einen parent) - ParentNode (hat eine Liste von children) und dann gibt es eben Nodes wie Element die sowohl ChildNode als auch ParentNode sind.

    Gefällt mir. Hier nutzt Du die Vererbung, um Fähigkeiten zu kombinieren, nämlich Kinder zu haben, bzw. Kind zu sein. Packst Du noch einen Marker DOMElement dazu, das Wurzel, Knoten und Blatt markiert, kannst Du Funktionen schreiben, die entweder nur Wurzeln, Knoten oder Blätter, Kinder habende, Kinder seiende oder eben DomElemente akzeptiert.

    Damit wären wir dann auch bei einer leeren Struktur "DomElement", die hier als "Marker" genutzt würde, was man gewissermaßen als vierte Idee beschreiben muss, für die "Vererbung" in C++ missbraucht wird, bzw. ein leeres Interface in Java/C#. Eine Instanz von DomElement würde allerdings keinen Sinn ergeben.

    SideWinder, über die Marker habe ich noch nicht als eigenständiges Konzept nachgedacht, aber für mich hat sich dieses Posting hiermit schon gelohnt. 🙂

    Shade Of Mine schrieb:

    Ob es einen parent hat oder nicht, ist doch eigentlich irrelevant. Warum genau muss ein Leaf Node anders behandelt werden als ein Non-Leaf Node?

    Ein konkretes Beispiel hat immer den Nachteil, dass es auch auf eine bestimmte Art anders zu lösen ist oder in dem Fall stellst Du Dir die Frage, ob das bei genau diesem Beispiel für Dich Sinn ergibt.

    Für andere kann der Unterschied aber durchaus relevant sein. Wenn Du eine Funktion schreibst, die nur Blätter akzeptiert, kannst Du das im Datentyp ausdrücken und musst dann keine Fragen stellen. Er muss auch nicht Fehlercodes oder Exceptions bearbeiten, die "falscher Nodetyp" bedeuten, weil er gar keine falschen Nodes übergeben kann.
    Weiß der Rufende, dass er nur Blätter hat, kann er das direkt zu Dir durchleiten und es werden niemals Fragen gestellt: Weniger Fragen => schnellerer Code.

    Shade Of Mine schrieb:

    Die Trennung Leaf/NonLeaf gibt es nicht. Wenn ich an einen Leaf Knoten einen Knoten dranhaenge ist er nicht mehr Leaf.

    Gerade im DOM gibt es sie.
    <h1>Hallo Welt</h1>
    Du kannst an das Blatt "Hallo Welt" keine Kinder anfügen.

    Shade Of Mine schrieb:

    Man kann auch einfach uebermodellieren.

    Das wiederum glaube ich nicht.

    Die Informationsmenge wächst und wird detaillierter. Die Information, dass es Leafs geben könnte, die niemals Kinder haben, ist Dir zuerst selbstverständlich vorgekommen, aber hättest Du es modelliert, hättest Du bei den String eben auch gemerkt, dass Du quasi untermodelliert hast.

    Ich glaube, man kann sich in einer Unmenge von Klassen verlieren, aber je genauer ein Typ beschreibt, was er kann oder eben nicht, desto besser. Die Frage, die man stellen darf und die auch zu stellen ist, ist ob C++ Vererbung ausreichend unterstützt, als dass man es uneingeschränkt empfehlen dürfte. Da wir an der Ableitung von int schon scheitern, geht uneingeschränkt ja schon gar nicht.

    Die Mehrfachvererbung "tagt" gewissermaßen Daten statisch, das bedeutet, das Tag kostet keine Rechenleistung, man kann mit falsch getagten Daten keinen Unsinn machen. Mit diesen Tags kann man aber Rechenleistung sparen, weil man eben keinen Member fragen muss, ob der "Tag" besteht oder nicht.

    In C++ besteht allerdings das Risiko, dass man einfach sehr viel Code produziert, um C++ die einzelnen Tags bekannt zu machen.

    Die Tags sind das große Potential von Mehrfachvererbung.

    Thingy schrieb:

    Allerdings sehe ich noch nicht ganz den Mehrwert, eher die Probleme, die auftreten. Wenn man dem Menschen noch einen weiteren Kopf hinzufügt, kompiliert Code nicht mehr, der früher problemlos ging; was heisst, dass hier die Kapselung sehr niedrig ist.

    Geniales Beispiel! Wenn ein Mensch zwei Köpfe hat, finde ich es super, wenn der Code nicht kompiliert. Das klingt nämlich nicht gesund.

    Der Mensch hat - wie Du richtig beschrieben hast - zwei Beine, aber es sind nicht zwei identische Beine. Sie befinden sich ja auch nicht am selben Ort: Das eine Bein ist links, das andere rechts. Und das beschreibst Du auch statisch über die beiden Strukturen LinkesBein und RechtesBein. Ein Mensch kann auch keine zwei identischen Köpfe haben. Wenn Du also einen zweiten Kopf willst, nötigt Dich der Compiler den Unterschied auch zu beschreiben und bietet Dir danach die Möglichkeit, Funktionen zu schreiben, die entweder nur den einen oder nur den anderen Kopf akzeptieren, so dass Du für wichtige Entscheidungen KopfZwischenSchultern verlangen kannst und nicht nur irgendein Member vom Typ Kopf, wo dann versehentlich das Member DenkOrganZwischenBeinen verwendet wird.
    Das vermeidet Bugs.

    volkards Posting verstehe ich nicht, aber mir scheint, dass es auch nicht zur Eingangsforderung passt:

    Kellerautomat schrieb:

    Ich bitte darum, die Diskussion konstruktiv fortzusetzen und nicht wieder gegenseitig aufeinander rumzuhacken - das fuehrt zu nichts.

    So, hackt los, dank SideWinders Post hat sich die Beantwortung hier für mich schon ausgezahlt, mir kann also nix mehr passieren. 😉



  • Xin schrieb:

    volkards Posting verstehe ich nicht

    Das ist ein ideologisches Problem.
    Lies meins bitte noch mehrmals ganz unvoreingenommen. Es müßte "klick" machen. Bitte, bitte.

    Ich habe mir so eine Mühe gegeben. Das MUSS man doch verstehen. Ich *kann* Dich auch mit Deinen Beispielen zu widerlegen versuchen, aber es dauert wirklich mir jedesmal zehnmal so viel Zeit, eine jedem (außer Dir) verständliche Widerlegung zu bauen, als Dir es dauert, den Müll reinzupusten. Das ist nicht fair. Und wenn Du dann alles ingnorierst, immer und immer wieder, haben wir konstant ein 1:10-Verhältnis. Das ist nicht ok. Sei mir nicht böse, aber da muss eine andere Basis her, um konstruktiv zu bleiben. Denke, eine Woche habe ich noch Geduld. Danach müssen wir aufgeben und zum Beispiel gegenseitig ein Fachbuch empfehlen, das der andere kauft, nebst Seitenangabe.



  • volkard schrieb:

    Xin schrieb:

    volkards Posting verstehe ich nicht

    Das ist ein ideologisches Problem.
    Lies meins bitte noch mehrmals ganz unvoreingenommen. Es müßte "klick" machen. Bitte, bitte.

    Dass Programme, die nicht fertig geschrieben sind, fehlerhaft funktionieren, halte ich für ziemlich normal.

    Ich fasse Deine Kritik aber durchaus auf, sehe da durchaus auch einen interessanten Beitrag, der aber nichts mit Mehrfachvererbung zu tun hat. Das kann Dir auch bei Sprachen passieren, die Mehrfachvererbung gar nicht unterstützen, dafür reicht Vererbung alleine und eben zu vergessen, eine Funktion entsprechend anzupassen.

    volkard schrieb:

    Ich habe mir so eine Mühe gegeben.

    Stimmt, für fünf Zeilen Text dreimal zu korrigieren, fällt eindeutig unter die Kategorie 'Mühe gegeben'.

    volkard schrieb:

    Denke, eine Woche habe ich noch Geduld. Danach müssen wir aufgeben und zum Beispiel gegenseitig ein Fachbuch empfehlen, das der andere kauft, nebst Seitenangabe.

    Das funktioniert nur unter zwei Bedingungen. Erstens: Es gibt ein sinnvolles Fachbuch und zweitens: Es ist nicht abgeschrieben.

    Das nennt sich wissenschaftliches Arbeiten. Die Diskussion hatte ich mit einem meiner Profs mal. Man stellt eine Behauptung auf und belegt sie damit, dass ein anderes Fachbuch diese These stützt. So arbeitet man wissenschaftlich.
    Blöderweise kommt man irgendwann am ersten Fachbuch an. Der Erste hat sich das aus den Fingern gesaugt, er kann nämlich nichts belegen.
    Mein Prof hatte damals ein Argumentationsproblem. Jedenfalls konnte er mir nicht erklären, wieso die zweite Autorengeneration richtig liegen muss, wenn der erste Generation keine Belege für ihre Behauptungen hat und dem Gesichtsausdruck nach, ist ihm das da zum ersten Mal aufgefallen. Er ist Professor, man könnte meinen, dass er sich mit wissenschaftlichen Arbeiten schonmal beschäftigt hat. Ich bin als Student da vergleichsweise Amateur.
    Ich bin sicher, Du löst dieses argumentative Problem.

    Ich schrieb bereits: Unser Verständnis von Programmierung hängt davon ab, was wir die letzten 60 Jahre getan haben, wenn wir programmiert haben. Für meine Sprache habe ich eine ausführliche Umfrage gemacht. Diejenigen, die am wenigsten zum Thema beizutragen hatten, waren Informatiker und erfahrene Programmierer. Die wissen nämlich, wie man's "richtig" macht, die machen das nämlich schon immer so und deswegen denken sie auch nichts anderes. Mathematiker, Ingenieure, Schüler haben da teilweise ganz andere Vorstellungen. Die sind in ihren Vorstellung nicht beschränkt. Die Informatiker fielen mit einem Satz auf "Habe ich mir nie Gedanken drüber gemacht.", denn sie haben ja gelernt, was richtig ist. Wir haben aber nie gelernt unser Verständnis in Frage zu stellen.

    Gib mir ein Argument, das ich noch nicht kenne. Fachbücher habe ich selbst genug. Gerade im Bereich Compilerbau und Sprachdesign steht in den meisten nicht viel Interessantes drin. Wenn Du was Gutes für mein Thema weist, nur zu.



  • Xin schrieb:

    volkard schrieb:

    Xin schrieb:

    volkards Posting verstehe ich nicht

    Das ist ein ideologisches Problem.
    Lies meins bitte noch mehrmals ganz unvoreingenommen. Es müßte "klick" machen. Bitte, bitte.

    Dass Programme, die nicht fertig geschrieben sind, fehlerhaft funktionieren, halte ich für ziemlich normal.

    lölchen 🤡
    Daß wergen Unfertigkeit Programme Fehler werfen, halte ich für normal. Daß sie einfach fehlerhaft laufen, halte ich für Sünde.
    HIER haben wir ein ganz entschiedenes Problem. Mir sind schon Projekte gestorben. Ich weiß, daß es darum geht, so zu programmieren, daß der Compiler Flüchtigkeitsfehler aufdeckt.

    Xin schrieb:

    Ich fasse Deine Kritik aber durchaus auf, sehe da durchaus auch einen interessanten Beitrag, der aber nichts mit Mehrfachvererbung zu tun hat. Das kann Dir auch bei Sprachen passieren, die Mehrfachvererbung gar nicht unterstützen, dafür reicht Vererbung alleine und eben zu vergessen, eine Funktion entsprechend anzupassen.

    Das ist auch mein Kritikpunkt.

    Mit

    class Address
      : protected Id
      , public    Street
      , public    HouseNumber
      , public    ZipCode
      , public    Town
    {};
    

    drückst Du aus, daß die Adresse eine Hausnummer IST (und nicht HAT). Mit allen KOnsequenzen. Alles, was eine Hausnummer KANN (Funktion), KANN auch eine Adresse tun. Das ist doch Unfug. Und tödlich.

    Java-mäßige Interfaces reinzuerben ist hingegen ok, so mehrfach Du magst. Selten schlau, aber auch selten tödlich.

    Xin schrieb:

    Ich bin als Student da vergleichsweise Amateur.
    Ich bin sicher, Du löst dieses argumentative Problem.

    Klar doch. Ganz pragmatisch: Es geht nicht anders. Worauf müßte sich zuguterletzt ein Richter im Zivilprozess verlassen, der kein eigenes Fachwissen hat? Bitte auf die Bücher und nicht auf die Wünschelrutengänger, Naturkeilkundler, Verschwörungstheoretiker und Mehrfachvererber.

    Xin schrieb:

    Ich schrieb bereits: Unser Verständnis von Programmierung hängt davon ab, was wir die letzten 60 Jahre getan haben, wenn wir programmiert haben. Für meine Sprache habe ich eine ausführliche Umfrage gemacht. Diejenigen, die am wenigsten zum Thema beizutragen hatten, waren Informatiker und erfahrene Programmierer.

    Mhhm. Vielleicht haben die erfahrenen Programmierer nicht ernst genommen?

    Xin schrieb:

    Die wissen nämlich, wie man's "richtig" macht, die machen das nämlich schon immer so und deswegen denken sie auch nichts anderes. Mathematiker, Ingenieure, Schüler haben da teilweise ganz andere Vorstellungen.

    Klar. Und junge Programmierer auch. Denen ist noch kein Projekt gestorben. :p

    Xin schrieb:

    Die sind in ihren Vorstellung nicht beschränkt. Die Informatiker fielen mit einem Satz auf "Habe ich mir nie Gedanken drüber gemacht.", denn sie haben ja gelernt, was richtig ist. Wir haben aber nie gelernt unser Verständnis in Frage zu stellen.

    Und deswegen hörst Du nicht auf gute Programmierer? Unlogisch. Was soll die Sprache werden? Eine, mit der sich Anfänger schnell anfreunden können, die aber ganz sicher nicht für nicht-winzig-kleine Programme taugt? Ähm haben wir mit Java doch schon. Gut, man müßte noch ein paar Fehler einbauen, um die Anforderung ganz zu erfüllen. MI wäre ganz toll, in der Tat.

    Xin schrieb:

    Gib mir ein Argument, das ich noch nicht kenne. Fachbücher habe ich selbst genug. Gerade im Bereich Compilerbau und Sprachdesign steht in den meisten nicht viel Interessantes drin. Wenn Du was Gutes für mein Thema weist, nur zu.

    Meyers, Effektiv C++ programmieren.

    So, und nur zum Positiven:

    struct ZipCode
      : public Integer
    {};
    

    Das ist eine gute Idee. Allerdings brauchen die eingebauten Typen auch einen gewissen Bestandsschutz. Wie das Verbot der Überladung von Operatoren für eingebaute Typen, denn sonst kommt so ein Schlawiener drauf und definiert die um und alles geht kaputt.
    Wenn ich jetzt den Wertebereich für ZipCodes einschränke auf nur 00000 bis 99999, dann können ZipCodes nicht mehr alles, was Integers können. Ich bin sicher, Dir fällt auf, wie bedenklich das ist.
    Du hast vererbt, Du hast VERSPROCHEN, daß jeder ZipCode alles kann, was ein Integer kann. Völlig unpassende Dinge wie die Rechenoperationen, falsche Dinge wie Ein-Ausgabe und gute Dinge: das Speicherlayout Konstruktorern, Destruktor und die Vergleichsoperationen. Ähm. Wir weh muss man Dir tun, bis Du anfängst, es andersrum zu machen? Zieh Dir doch nur die Sachen rein, die Du haben magst und als richtig auch absegnen kannst und nur die.



  • @Xin: Wie würde man in deiner Sprache die Klassen "Rectangle" und "Square" in einem Vektorzeichenprogramm definieren. Könnte man diese voneinander ableiten?

    MfG SideWinder



  • SideWinder schrieb:

    @Xin: Wie würde man in deiner Sprache die Klassen "Rectangle" und "Square" in einem Vektorzeichenprogramm definieren. Könnte man diese voneinander ableiten?

    Doppelspoiler. http://pastebin.de/33083



  • SideWinder schrieb:

    @Xin: Wie würde man in deiner Sprache die Klassen "Rectangle" und "Square" in einem Vektorzeichenprogramm definieren. Könnte man diese voneinander ableiten?

    Wen ableiten wovon?

    Rechteck "ist ein" Quadrat mit zusätzlicher Komponente? Quadrat "ist ein" Rechteck mit zwei Komponenten, die identisch sein müssen?

    Das hat nichts mit meiner Sprache zu tun, das ist eine sprachunabhängige Designfrage. Ich würde es wohl so modellieren, wobei das hier kein Beispiel von Codeeffizienz darstellt, sondern ein Beispiel meiner Denke:

    class X
      : public Double {}
    
    class Y
      : public Double {}
    
    class Punkt
      : public X
      , public Y {}
    
    class Greifpunkt
      : public Punkt {}
    
    class Grafikobjekt
      : public Greifpunkt {}
    
    class ViereckigesGrafikObjekt
      : public Grafikobjekt {}
    
    class Strecke
      : public Double {}
    
    class Seitenlänge
      : public Strecke {}
    
    class SeitenlängeX
      : public Strecke {}
    
    class SeitenlängeY
      : public Strecke {}
    
    class SeitenlängeXY
      : public SeitenlängeX {}
      : public SeitenlängeY {}
    
    class Radius
      : public Strecke {}
    
    class Quadrat
      : public ViereckigesGrafikObjekt
      , public Seitenlänge {}
    
    class Rechteck
      : public ViereckigesGrafikObjekt
      , public SeitenlängeXY
     {}
    
    class Kreis
      : public GrafikObjekt
      , public Radius {}
    

    Jetzt sagst Du natürlich erstmal zurecht "Was ist das für'n Scheiß, da klassifiziere ich mich ja kaputt, das geht mit Membern VIEL schnell". Das ist richtig, aber Du kannst aber viel weniger Informationen statisch im Datentyp transportieren. Ein Double ist XKoordinate, YKoordinate, Seitenlänge für X und Y oder Seitenlänge für X oder Y... Funktionen fragen immer nur nach double. Möchte ich die Kreisfläche berechnen, frage ich nach Radius, nicht nach double. Ich kann auch nicht versehentlich Quadrate reinwerfen, die ja genauso wie Kreise sind: Ein Punkt mit zusätzlichem Double.
    Hier kam das Argument auf, dass die IDE für das Schreiben von Code hilfreich ist. Leider kann die IDE aber nicht bestimmen, welche Funktion die richtige ist und manchmal findet sie zuerst die falsche, dann übergebe ich mein Double und der Bug ist fertig. Das geht hier nicht. Ein Viereck hat keinen Radius => der Compiler verweigert die Kompilierung.

    In meiner Sprache ist das bedeutend kürzer. Stell Dir das hier nicht als Ziel, sondern als Ausgangspunkt vor, an Du losgehst. Ich schrieb, dass ich in der Mehrfachvererbung Potential sehe, nicht, dass C++ es optimal nutzt. Dies zeigt das zu erreichende Ziel, wie man es in C++ formulieren muss - also leider mit viel Boilerplate: Jedes Datum hat seinen eigenen, eindeutigen Typ.

    Ich stelle meine Sprache vor, wenn ich der Meinung bin, es zeigen zu wollen. Für dieses Konzept habe ich die Syntax komplett umgebaut und vielleicht fällt mir irgendwann noch etwas Interessantes ein, was wieder Änderungen mit sich bringt.

    Ich habe noch nicht alles ausprobiert und ich habe keinen Bock mit volkard & Co rumzudiskutieren, ob meine Syntax jetzt schon perfekt ist oder nicht. Ich muss so oder so ausprobieren und in der Zwischenzeit hat volkard mir schon oft genug erklärt, dass er - freundlich formuliert - meinen Ansatz nicht schätzt.
    Also probiere ich lieber mit ausgewähltem Publikum, das spart mir Zeit. Das Posting gestern waren 2 Stunden, die ich auch für anderes gut hätte gebrauchen konnte - sich aber dank Dir wenigstens gelohnt haben, weil es eben doch immer mal wieder noch kleine "Aha"-Effekte gibt, wenn sich Leute konstruktiv mit den Dingen auseinander setzen. Bedauerlicherweise wissen die meisten aber schon, wie man es richtig macht. Nicht umsonst haben wir soviele Bundestrainer im Land 😉

    EDIT:
    Ich habe diese Klassenhierarchie jetzt mal in meiner Sprache definiert, das sind 9 (durchaus lesbare) Zeilen in der kürzesten Form, bzw. 17, wenn ich die Leerzeilen zwischen jeder einzeiligen Definition dazupacke. Es sind weniger explizite Definitionen erforderlich. 11 Zeilen, wenn ich die Mehrfachvererbungen untereinander schreibe, also 19 mit Leerzeilen.



  • Wie genau verhinderst du jetzt dass ich den Radius von einem Kreis negativ setze?



  • Shade Of Mine schrieb:

    Wie genau verhinderst du jetzt dass ich den Radius von einem Kreis negativ setze?

    <sarcasm>
    Ich glaube, Du hast da ein wesentliches K.O. Kriterium erkannt.

    Man kann auch keinen Kaffee damit kochen. Ich glaube, ich sollte die Programmierung aufgeben, wenn's nichtmals Kaffee kochen kann.
    </sarcasm>

    PS: Wenn das jetzt ernsthaft die schärfste Kritik von Dir war, die Dir für dieses Beispiel eingefallen ist, dann kann ich so falsch ja gar nicht liegen.



  • Ich fang an zu glauben, dass Xin prädikativ Typen will wie in Cecil.



  • Xin schrieb:

    PS: Wenn das jetzt ernsthaft die schärfste Kritik von Dir war, die Dir für dieses Beispiel eingefallen ist, dann kann ich so falsch ja gar nicht liegen.

    Es ist eine ernste Frage.
    Ich stelle sie mal allgemeiner: wie garantierst du Invarianten?

    Wenn Postleitzahl von Integer erbt, wie beschraenke ich Postleitzahlen auf 0 bis 99999 ? Oder woher weiss der Kreis, dass sich sein Ursprung geaendert hat um sich neu zuzeichen? Etc.



  • Zeus schrieb:

    Ich fang an zu glauben, dass Xin prädikativ Typen will wie in Cecil.

    Cecil? Da finde ich Damenmode!? ^^

    Habe noch eine Mono-Library gefunden.

    Shade Of Mine schrieb:

    Es ist eine ernste Frage.
    Ich stelle sie mal allgemeiner: wie garantierst du Invarianten?

    Wie garantiere ich Invarianten mit Membern?

    Wenn ich einem Kreis ein double als Radius mitgebe, wie garantiere ich da, dass der Kreis einen positiven Radius besitzt?

    Shade Of Mine schrieb:

    Wenn Postleitzahl von Integer erbt, wie beschraenke ich Postleitzahlen auf 0 bis 99999 ?

    In dem Fall muss ich wohl für PLZ einen Konstruktor schreiben, der das verifiziert und eine Initialisierung mit unzulässigen Werten ggfs. ablehnt, wie ich für Radius auch tun müsste, wenn ich ein Problem mit negativen Radien habe.

    Ich sehe hier keinen Unterschied zur Initialisierung von Membern in der Initialisierungsliste - und, je nach Implementierung auch nicht im Konstruktor der Address-Klasse.

    Shade Of Mine schrieb:

    Oder woher weiss der Kreis, dass sich sein Ursprung geaendert hat um sich neu zuzeichen? Etc.

    Auch hier sehe ich das Problem erstmal nicht im Aufbau der Datentypen, Du kannst genauso Events verwenden oder eben den Zugriff auf den Schreibzugriff von Punkt im Grafikobjekt erstmal abfangen. Das wäre allerdings in C++ wieder nicht formulierbar, ohne dass man überladbare Methoden hat. Ich selbst mache zwischen Getter- und Setter-Funktionen und dem direkten Zugriff auf Variablen keinen so großen Unterschied.

    Aber es gibt vermutlich noch eine weitere vorstellbare Lösung, die in C++ aber gar nicht formulierbar ist und ich bei mir noch nicht ausprobieren konnte. Soweit ist meine Sprache leider noch nicht. Vielleicht kann ich dazu Ende des Jahres was sagen.



  • Xin schrieb:

    Shade Of Mine schrieb:

    Wenn Postleitzahl von Integer erbt, wie beschraenke ich Postleitzahlen auf 0 bis 99999 ?

    In dem Fall muss ich wohl für PLZ einen Konstruktor schreiben, der das verifiziert und eine Initialisierung mit unzulässigen Werten ggfs. ablehnt, wie ich für Radius auch tun müsste, wenn ich ein Problem mit negativen Radien habe.

    Hier sehe ich zwei Probleme. Das erste ist sowas (Pseudocode):

    func(Integer& i1, Integer i2) {
        i1 -= i2;  // gültig und definiert für Integer
    }
    
    func(kreis1.radius, kreis2.radius); // huch, plötzlich Fehler, obwohl ein Radius ein Integer ist
    

    Radius ist hier Integer statt Double der Einfachheit halber, das Argument ist dasselbe.
    Das Problem hat man mit Membern nicht, da bei

    kreis1.setRadius(kreis1.getRadius() - kreis2.getRadius())
    

    eben nicht garantiert ist, dass das keinen Fehler wirft, im Gegensatz zum -= bei Integern.

    Das zweite ist, dass es bei deiner Klassenhierarchie ja eigentlich heißen müsste:

    func(kreis1, kreis2)
    

    Aber welcher Integer beim Kreis ist denn gemeint? Radius, Durchmesser, Umfang? Gut, hier könnte man es sich eventuell denken, aber was ist beim Rechteck?
    Das „is-a“, was immer so runtergebetet wird, hat ja einen Grund: A is-a B ⇒ A kann wie ein B behandelt werden. Und ich sehe hier nicht, dass ich einen Kreis oder ein Rechteck wie einen Integer behandeln kann.



  • Xin schrieb:

    In dem Fall muss ich wohl für PLZ einen Konstruktor schreiben, der das verifiziert und eine Initialisierung mit unzulässigen Werten ggfs. ablehnt, wie ich für Radius auch tun müsste, wenn ich ein Problem mit negativen Radien habe.

    Ich sehe hier keinen Unterschied zur Initialisierung von Membern in der Initialisierungsliste - und, je nach Implementierung auch nicht im Konstruktor der Address-Klasse.

    Was wenn ich jetzt aber:

    void add(Integer& lhs, Integer& rhs) {
      lhs+=rhs;
    }
    
    Postleitzahl plz(12345);
    add(plz, 700000);
    

    schreibe?

    Der Ctor von Postleitzahl validiert 12345 korrekt und es passt. Nun addiert add aber 700000 drauf und wir haben eine ungueltige Postleitzahl. Was tust du hier um das zu verhindern?

    In Postleitzahl kannst du den operator+= fuer Integer ja nicht neu definieren - es sei denn der waere virtual. Ist er das?

    Bzw: wie verhinderst du, dass ich Sachen mache die keinen Sinn ergeben: zB eine Postleitzahl mit einer Telefonnummer zumultiplizieren?

    Mich interessiert dabei die Umsetzung in C++ (wir koennen uns auch gerne auf eine andere Sprache einigen). Deine Sprache wuerde ich aus der Diskussion lieber raus lassen, da es keine Sprache ist die wir beide kennen.



  • ipsec schrieb:

    Xin schrieb:

    Shade Of Mine schrieb:

    Wenn Postleitzahl von Integer erbt, wie beschraenke ich Postleitzahlen auf 0 bis 99999 ?

    In dem Fall muss ich wohl für PLZ einen Konstruktor schreiben, der das verifiziert und eine Initialisierung mit unzulässigen Werten ggfs. ablehnt, wie ich für Radius auch tun müsste, wenn ich ein Problem mit negativen Radien habe.

    Hier sehe ich zwei Probleme. Das erste ist sowas (Pseudocode):

    func(Integer& i1, Integer i2) {
        i1 -= i2;  // gültig und definiert für Integer
    }
    
    func(kreis1.radius, kreis2.radius); // huch, plötzlich Fehler, obwohl ein Radius ein Integer ist
    

    Radius ist hier Integer statt Double der Einfachheit halber, das Argument ist dasselbe.

    Das Argument ist sehr gut und bekannt.

    Fakt ist, dass ein Klasse, die eine Einschränkung ihrer Basis hat, natürlich nicht public erben darf. Ein Kreis mit einem öffentlichen Member double Radius kann auch zerlegt werden und zwar genauso, wie Du es beschrieben hast. Es gilt also Inhalt von Integer zu schützen.

    Eine Klasse Radius darf diese Operationen also nicht öffentlich erben, wenn sie Einschränkungen macht. Im Prinzip ist das eine protected Ableitung mit einem operator Integer & const(). Radius darf Integer modifizieren, externe haben kein Zugriffsrecht. Du kannst zum Beispiel einen (Integer const &) zurückgeben, aber den bekommst Du nicht mit dem -= Operator verbogen. Also entweder beschreibst Du in Radius, was Du mit dem Radius machen darfst oder Du kannst ihn nicht in eine Funktion packen,

    ipsec schrieb:

    Das zweite ist, dass es bei deiner Klassenhierarchie ja eigentlich heißen müsste:

    func(kreis1, kreis2)
    

    Aber welcher Integer beim Kreis ist denn gemeint? Radius, Durchmesser, Umfang? Gut, hier könnte man es sich eventuell denken, aber was ist beim Rechteck?

    Auch ein schöner Punkt, der aber durchaus auch in C++ bekannt und gelöst ist. Wenn Du das in C++ so formulierst, wird Dir C++ sagen, dass er eben auch nicht weiß, welchen Integer er hier jetzt reinwerfen soll. Da die Funktion nicht klar festlegt, welchen Integer sie wünscht, also ob Radius, Durchmesser oder Umfang, muss sie ja so allgemein gehalten sein, dass ihr ein beliebiger Integer zur Verarbeitung reicht.
    Und da haben wir natürlich Auswahl und müssen uns selbst festlegen.
    In C++ müsstest Du Dich über ein Casting eindeutig ausdrücken. Da sieht in C++ unschön aus, ist aber an der Stelle das Gleiche wie this->Radius: eine Addition auf die Adresse auf die this zeigt.

    ipsec schrieb:

    Das „is-a“, was immer so runtergebetet wird, hat ja einen Grund: A is-a B ⇒ A kann wie ein B behandelt werden. Und ich sehe hier nicht, dass ich einen Kreis oder ein Rechteck wie einen Integer behandeln kann.

    Bitte die Bibel weglegen und das beten einstellen. Die Gedanken sind frei! 😉

    "is-a" ist nur ein Konzept, dass mit Vererbung beschrieben wird. Stell Dir Kreis als Ableitung von RadiusInterface vor. Damit ist Kreis ein "GetRadiusFunctionProvider" und das wiederum ist ein Integer. Und jetzt lassen wir den ganzen Unsinn weg wieder und sagen "einfach":

    printInteger( this->Radius::Value );
    oder
    printInteger( this->Umfang::Value );

    Wohlgemerkt: Das ist C++... die Sprache, die für Member ausgelegt ist und ich behaupte, dass Mehrfachvererbung und Member erstmal beide die gleiche Aufgabe erfüllen. Das heißt nicht, dass die Syntax in C++ wunderschön ist, noch dass ich ausschließlich alles public machen will. Ich will nur Vorteile von Mehrfachvererbung allgemeiner nutzen.

    Trotzdem: das war bisher eins der besten Postings, die zu dem Thema jemals gekommen sind.



  • Xin schrieb:

    Eine Klasse Radius darf diese Operationen also nicht öffentlich erben, wenn sie Einschränkungen macht.

    OK. Jetzt verwirrst du mich.
    Kannst du dein Rechteck/Kreis Beispiel dann nochmal herzeigen mit protected Vererbung?

    Weil bis jetzt bin ich von public Vererbung ausgegangen...



  • -.- bitte löschen, danke



  • Xin schrieb:

    Fakt ist, dass ein Klasse, die eine Einschränkung ihrer Basis hat, natürlich nicht public erben darf.

    Und private auch nicht.
    Deswegen schrieb ich mein Beispiel so, daß der Mensch tot wird aus einer Mensch::-Funktion aufgerufen! Auch privat haben wir den Ärger, wenn man Konzepte vermischt. Plötzliches Frühableben ist ehrlich suboptimal.

    Zugegeben, C++ ist schwach und unterstützt native weder Mixins noch Interfaces.

    Du solltest nicht den Fehler begehen, das zu ignorieren. Mach doch Sprachmittel dafür. Du kannst es. Für beide!


Anmelden zum Antworten