Rust



  • TyRoXx schrieb:

    Wer sich mit modernem C++ auseinandergesetzt hat, wird den Sinn von Rust verstehen.

    Ich zweifle den Sinn nicht unbedingt an, aber wer unbedingt eine Meinung aus C++ Entwicklersicht haben will, muss sich entsprechende Antworten nun einmal anhören (Und ganz abwegig ist es auch nicht die Aspekte Erfahrung und Gewohnheit zu berücksichtigen - das sind auch Probleme an denen in der Praxis viele gute Sprachansätze im Endeffekt scheitern).



  • Mir gefaellt die Sprache und ich benutze sie so mehr oder weniger seit einem halben Jahr. Man ist sogar erstaunlich produktiv, trotz der Sicherheitsbeschraenkungen.
    Insbesondere die enums sind hilfreich, weil man viele Faelle, in denen man mehrere moegliche Typen hat, nicht mit Vererbung oder verschachtelten if-konstruktionen gut in eine Klasse gekapselt hat, sondern direkt in einem match statement aufloesen kann.



  • Mir gefällt sie auch sehr und steht nach Haskell als nächstes auf meiner TODO Liste zum lernen.



  • Tolles Sprachenkonzept, was einige heftige menschliche Fehler per Compiler verhindert. Auf jeden Fall einen Sprache für alles was in einem Netzwerk agiert und schnell sein muss.

    Manchmal muss man das Alte abbrennen, damit Neues entstehen kann.



  • OverflowKiller schrieb:

    Manchmal muss man das Alte abbrennen, damit Neues entstehen kann.

    Das hat man mir schon vor 30 Jahren erzählt. "ADA, COBOL, FORTRAN sind tot vieleicht noch ein, zwei Jahre".



  • Sind sie ja auch, hat man damals also wohl recht gehabt.



  • Und was ist mit den mio. LOC die heute noch in Finanzbehörden und Banken ihr Unwesen treiben?



  • Hat aber gedauert, ich habe hier noch bis vor zwei Jahren auch mit Fortran arbeiten müssen.



  • Solange es keine IDE gibt, fasse ich Rust auch nicht an.
    Und bei somanchen syntaktischen Konstrukten koennte ich kotzen.



  • Nur dass das nicht missverstanden wird, aber welche Art von Konstrukten meinst du. Möchte jetzt nämlich nicht rust ausprobieren müssen um dahinter zu kommen was du meinen könntest.



  • andreasgeorg schrieb:

    OverflowKiller schrieb:

    Manchmal muss man das Alte abbrennen, damit Neues entstehen kann.

    Das hat man mir schon vor 30 Jahren erzählt. "ADA, COBOL, FORTRAN sind tot vieleicht noch ein, zwei Jahre".

    Habe erst, auf dem Brett meiner Hochschule, eine Studienarbeit zur Entwicklung eines Cobol-Formatters gesehen ;).



  • Der Grund, warum ich Rust noch nicht viel einsetze, hat nix mit den hier genannten Sorgen zu tun. Es liegt einfach daran, dass in der Doku zur Standardbilbiothek noch zu oft "unstable" steht und die Zahl an Bibliotheken anderer Leute noch sehr überschaubar ist. Deswegen ist mein aktuelles Hobby-Projekt in C++, wo ich Boost.ASIO drin verwenden kann.

    Kellerautomat schrieb:

    Und bei somanchen syntaktischen Konstrukten koennte ich kotzen.

    simbad schrieb:

    Nur dass das nicht missverstanden wird, aber welche Art von Konstrukten meinst du. Möchte jetzt nämlich nicht rust ausprobieren müssen um dahinter zu kommen was du meinen könntest.

    Eigentlich ist die Syntax insgesamt harmloser als das, was dir in C++ zugemutet wird. Man gewöhnt sich schnell um. Und vieles lässt sich dann leichter lesen und schreiben.

    Mir fallen aber auch zwei Eigenschaften von Rust ein, die im Vergleich zu C++ zu mehr Code führen oder syntaktisch "komisch" aussehen können. Das erste ist: Rust bietet kein Overloading. Du kannst also nicht schreiben

    fn tcp_connect(sa: SocketAddr) -> io::Result<TcpStream> {
        …
    }
    
    fn tcp_connect(a: IpAddr, port: u16) -> io::Result<TcpStream> {
        tcp_connect(SocketAddr::new(a, port))
    }
    
    fn tcp_connect(sa: &str) -> io::Result<TcpStream> {
        let (host, port) = parse_stuff(sa);
        let ip = hostname_to_ipaddr(host);
        tcp_connect(ip, port)
    }
    

    Was man stattdessen machen kann, sieht in etwa so aus

    trait ToSocketAddr {
        fn to_socket_addr(&self) -> SocketAddr;
    }
    
    impl ToSocketAddr for SocketAddr {
        fn to_socket_addr(&self) -> SocketAddr {
            *self
        }
    }
    
    impl ToSocketAddr for (IpAddr, u16) {
        fn to_socket_addr(&self) -> SocketAddr {
            SocketAddr::new(self.0, self.1)
        }
    }
    
    impl ToSocketAddr for str {
        fn to_socket_addr(&self) -> {
            let (host, port) = parse_stuff(self);
            let ip = hostname_to_ipaddr(host);
            (ip, port).to_socket_addr()
        }
    }
    
    fn tcp_connect<A: ToSocketAddr>(a: &A) -> io::Result<TcpStream> {
        let sa = a.to_socket_addr();
        …
    }
    

    Einerseits ist das nett. Es zwingt dich dazu, tcp_connect nur einmal zu schreiben (don't repeat yourself), so dass nur die Unterschiede quasi ausgelagert werden. Das macht die trait-Implementierungen auch gleichzeitig an anderen Stellen nutzbar (z.B. auch beim Öffnen eines UDP-Sockets) was auch förderlich im Sinne von DRY ist. Eine wichtige Motivation für diesen Ansatz ist die mächtige Typ-Inferenz (Hindley-Milner), die mit Overloading wahrscheinlich nicht so toll interagieren würde. Andererseits brauche ich in diesem Beispiel offensichtlich ohne Overloading mehr Zeilen. Es ist also eine Stelle, wo einem eine "Sauberkeit" quasi aufgewzgunen wird, die ggf zu mehr Zeilen Code führt.

    Und das zweite, was mir da einfällt sind die Lebenszeitparameter, die man manchmal explizit hinschreiben muss. Das ist ja ein Konzept, was C++ nicht kennt. Es ist aber in Rust nötig, um Speichersicherheit ohne Garbage Collection garantieren zu können. Es ist nötig, im Typsystem etwas über Lebenszeiten sagen zu können. Meistens muss man Lebenszeiten nicht explizit benennen:

    struct Person { vorname: String, nachname: String }
    
    fn foo(p: &Person) -> &str { &p.vorname }
    fn bar(p: &Person) -> &str { &p.nachname }
    

    Es funktioniert einfach auf magische Weise und der Compiler kann einem trotzdem garantieren, dass die Referenzen, die foo und bar zurückgeben und sich auf das "Innere" eines Person-Objekts beziehen, immer gültig bleiben (sofern der Compiler das Programm akzeptiert). Wie funktioniert das jetzt? Der Trick ist, dass hier Lebenszeitparameter im Spiel und Teil des Typsystems sind. Wir haben hier ein oft vorkommendes Muster: Die Lebenszeit dessen worauf sich das Ergebnis von foo und bar bezieht, ist dieselbe wie die Lebenszeit der Referenz, die als Argument übergeben wird. Explizit aufgeschrieben heißt das

    fn foo<'a>(p: &'a Person) -> &'a str { &p.vorname }
    fn bar<'a>(p: &'a Person) -> &'a str { &p.nachname }
    

    Es sind also tatsächlich Funktionsfamilien, polymorph über den Lebenszeitparameter 'a. Wenn man von diesem "Eingabe=Ausgabe"-Muster abweicht, muss man auch explizit werden. Beispiel:

    fn min<'a, T: Ord>(x: &'a T, y: &'a T) -> &'a T {
        if x < y {
            x
        } else {
            y
        }
    }
    

    Hier würde 'a vom Compiler als maximale Lebenszeit deduziert, die die Lebenszeit von *x und von *y nicht überschreitet. Der Compiler weiß damit anhand der Signatur (also ohne die Implementierung zu kennen), dass sich die zurückgegebene Referenz nicht länger als das Minimum der Lebenszeit von *x und *y am leben bleiben darf. Das ist vielleicht nicht sofort nachzuvollziehen. Aber Referenztypen sind Kontravariant bzgl des Lebenszeitparameters. Man darf also den Lebenszeitparameter eines Referenzwertes verkleinern, ohne dass dabei was schief gehen kann.

    Weil man hier bei dieser Funktion nicht drum herum kommt, explizit was zu Lebenszeiten zu sagen, ist das natürlich eine Sache, die syntaktisch umständlicher als in C++ aussieht. Aber wir machen das ja auch nicht umsonst!

    HTH,
    kk



  • simbad schrieb:

    Nur dass das nicht missverstanden wird, aber welche Art von Konstrukten meinst du. Möchte jetzt nämlich nicht rust ausprobieren müssen um dahinter zu kommen was du meinen könntest.

    Was mir gerade so einfaellt, was ich an Rust nicht mag:

    • Returns ohne 'return'
    • Implizites const
    • Erzwungene {}
    • Implizites move
    • unions versteckt als enums
    • Pattern matching

    Gibt bestimmt noch einiges mehr.



  • Kellerautomat schrieb:

    • Returns ohne 'return'

    Unwichtiges, syntaktisches Detail, woran man sich schnell gewöhnt.

    Kellerautomat schrieb:

    • Implizites const
    • Erzwungene {}
    • Implizites move

    Das sind eindeutige Vorteile gegenüber C++.

    Kellerautomat schrieb:

    • unions versteckt als enums
    • Pattern matching

    union in C ist etwas anderes als enum in Rust.
    Pattern matching ist die längst überfällige Überführung von switch ins 21. Jahrhundert.

    Kellerautomat schrieb:

    Gibt bestimmt noch einiges mehr.

    Du kennst Rust nicht, also fürchtest du es erst einmal. Ist doch ganz normal.



  • Kellerautomat schrieb:

    Erzwungene {}

    Das geht echt gar nicht! Viel schlimmer als die (), die du in C und C++ für if, while und for brauchst. 😉

    Und so Fehler wie das "goto fail;" gibt's in der Realität auch nicht. {} statt () ist also eine voll bescheuerte Idee und richtet sich gegen meine Gewohnheit! Wie können die nur? 😉



  • Implizites move

    Gibt es in C++ auch. Beispielsweise beim return.

    void f()
    {
        std::string s;
        return s; // Falls copy-elision nicht greift wird auf jeden Fall der Move-Konstruktor genommen.
    }
    

    Gleiches gilt für throw-expressions.

    Implizites const

    Gibt's auch bei C++ ab und an.

    Returns ohne 'return'

    Naja. So schlimm? Unkonventionell halt. Außerdem kannst du return verwenden. Es wäre AFAICS nützlich für süße kleine Funktionen, bei denen return ; schon 25% des Funktionskörpers ausmachen würde.

    TyRroXX (ich kann seinen Namen niemals korrekt buchstabieren) schrieb:

    Das sind eindeutige Vorteile gegenüber C++.

    Du meinst, abgesehen vom mittleren Punkt?

    Außerdem denke ich bei jeder Sprache die

    let program = "+ + * - /";
    

    erlaubt an "Skriptkiddie-Scheiß".

    Gibt es bei der Sprache aber auch irgendwelche objektiven Vorteile? Edit: Ach, hier ist's gelistet: http://www.rust-lang.org/



  • Kellerautomat schrieb:

    Pattern matching

    Meinst du da das Konzept an sich oder die syntaktische Umsetzung?



  • Arcoth schrieb:

    TyRroXX (ich kann seinen Namen niemals korrekt buchstabieren) schrieb:

    Das sind eindeutige Vorteile gegenüber C++.

    Du meinst, abgesehen vom mittleren Punkt?

    Naja, das ist eher eine Geschmacksfrage als alles andere. Der {}-Zwang spart dir dafür die runden Klammern bei if, for, while. Es nimmt sich also vom Tippaufwand her nichts. Jetzt kann man sich fragen und drüber streiten, welcher Ansatz vielleicht weniger fehleranfällig oder konsistenter ist … aber ich denke nicht, dass das irgendwohin führt…

    Arcoth schrieb:

    Implizites const

    Gibt's auch bei C++ ab und an.

    Ich wüsste nicht wo. Meinst du vielleicht Regeln, die auf die Wertkategorie abzielen? (Prvalue --/-> non-const ref)

    Arcoth schrieb:

    Außerdem denke ich bei jeder Sprache die

    let program = "+ + * - /";
    

    erlaubt an "Skriptkiddie-Scheiß".

    Das kann man ja eigentlich positiv werten, zumal es eben kein "Skriptkiddie-Scheiß" ist. 😃 Boilerplate Code ist ja jetzt nicht unbedingt wünschenswert.



  • Ich wüsste nicht wo.

    Moment, von was für impliziten const s reden wir eigentlich? Ich dachte an etwas wie implizites const bei den operator() s von closure Typen.



  • Arcoth schrieb:

    Ich dachte an etwas wie implizites const bei den operator() s von closure Typen.

    Ja stimmt! Da ist ein implizites const. Hab ich nicht dran gedacht. Und wenn man das nicht haben will, muss man mutable verwenden. So ist das bei Rust im Prinzip fast überall:

    &T     --> shared reference (erlaubt i.d.R. nur "lesenden" Zugriff)
    &mut T --> mutable reference (erlaubt auch Schreibzugriff)
    
    fn main() {
        let x = 23;      // per default immutable
        let mut y = 42;  // kann ich nachträglich ändern
    }
    

Anmelden zum Antworten