Portieren des Befehlszeilenprogramms von Go / Rust nach D.

Vor einigen Tagen teilte Paulo Henrique Cuchi auf einem Reddit in "Programmierung" seine Erfahrungen bei der Entwicklung eines Befehlszeilenprogramms in Rust and Go ( übersetzt in Habré ) mit. Das fragliche Dienstprogramm ist ein Kunde für sein Haustierprojekt Hashtrack. Hashtrack bietet eine GraphQL-API, mit der Kunden bestimmte Twitter-Hashtags verfolgen und in Echtzeit eine Liste relevanter Tweets abrufen können. Auf einen Kommentar hin habe ich beschlossen, einen Port in D zu schreiben, um zu demonstrieren, wie D für ähnliche Zwecke verwendet werden kann. Ich werde versuchen, die gleiche Struktur beizubehalten, die er in seinem Blogbeitrag verwendet hat.



Quellen auf GitHub



Video auf Klick



Wie ich zu D kam



Der Hauptgrund ist, dass der ursprüngliche Blogpost statisch typisierte Sprachen wie Go und Rust verglich und respektvolle Verweise auf Nim und Crystal machte, aber D nicht erwähnte, das ebenfalls in diese Kategorie fällt. Ich denke, das wird den Vergleich interessant machen.



Ich mag auch D als Sprache und habe es in verschiedenen anderen Blog-Posts erwähnt.



Unmittelbare Umgebung



Das Handbuch enthält ausführliche Informationen zum Herunterladen und Installieren des Referenz-Compilers DMD. Windows-Benutzer können das Installationsprogramm herunterladen, während MacOS-Benutzer Homebrew verwenden können. Unter Ubuntu habe ich gerade das apt-Repository hinzugefügt und bin der normalen Installation gefolgt. Damit erhalten Sie nicht nur das DMD, sondern auch den Dub, den Paketmanager.



Ich habe Rust installiert, um eine Vorstellung davon zu bekommen, wie einfach es wäre, loszulegen. Ich war überrascht, wie einfach es ist. Ich musste nur das interaktive Installationsprogramm ausführen , das sich um den Rest kümmerte. Ich musste ~ / .cargo / bin zum Pfad hinzufügen. Sie mussten nur die Konsole neu starten, damit die Änderungen wirksam wurden.



Unterstützung durch Redakteure



Ich habe den Hashtrack in Vim ohne große Schwierigkeiten geschrieben, aber das liegt wahrscheinlich daran, dass ich eine Vorstellung davon habe, was in der Standardbibliothek vor sich geht. Ich hatte die Dokumentation immer geöffnet, weil ich manchmal ein Symbol verwendet habe, das ich nicht aus dem richtigen Paket importiert habe, oder eine Funktion mit den falschen Argumenten aufgerufen habe. Beachten Sie, dass Sie für die Standardbibliothek einfach "import std" schreiben können. und alles zur Verfügung haben. Für Bibliotheken von Drittanbietern sind Sie jedoch allein.



Ich war neugierig auf den Status des Toolkits und habe mir Plugins für meine Lieblings-IDE, Intellij IDEA, angesehen. Ich habe das gefundenund installiert. Ich habe auch DCD und DScanner installiert, indem ich ihre jeweiligen Repos geklont und erstellt und dann das IDEA-Plugin so konfiguriert habe, dass es auf die richtigen Pfade verweist. Wenden Sie sich zur Klärung an den Autor dieses Blogposts .



Anfangs hatte ich einige Probleme, die jedoch nach dem Aktualisieren der IDE und des Plugins behoben wurden. Ein Problem, auf das ich stieß, war, dass sie meine eigenen Pakete nicht erkennen konnte und sie immer wieder als "möglicherweise undefiniert" markierte. Später stellte ich fest, dass ich "module package_module_name" eingeben musste, damit sie erkannt wurden. am Anfang der Datei.



Ich denke, es gibt immer noch einen Fehler, bei dem .length zumindest auf meinem Computer nicht erkannt wird. Ich habe eine Ausgabe auf Github eröffnet. Sie können sie hier verfolgenwenn du neugierig bist



Wenn Sie unter Windows arbeiten, habe ich gute Dinge über VisualD gehört .



Paketverwaltung



Dub ist der De-facto-Paketmanager in D. Er lädt Abhängigkeiten von code.dlang.org herunter und installiert sie . Für dieses Projekt benötigte ich einen HTTP-Client, da ich cURL nicht verwenden wollte. Am Ende hatte ich zwei Abhängigkeiten, Anfragen und deren Abhängigkeit, Cachetools, die keine eigene Abhängigkeit haben. Aus irgendeinem Grund hat er jedoch zwölf weitere Abhängigkeiten ausgewählt:







Ich denke, Dub verwendet sie intern, bin mir aber nicht sicher.



Rust hat viele Kisten geladen ( ca. 228 ), aber das liegt wahrscheinlich daran, dass die Rust-Version mehr Funktionen bietet als meine. Zum Beispiel hat er rpassword heruntergeladen , ein Tool, das Kennwortzeichen verbirgt, während er sie in das Terminal eingibt, ähnlich wie die getpass-Funktion von Python.Dies ist eines der vielen Dinge, die ich nicht im Code habe. Dank dieser Empfehlung habe ich die getpass-Unterstützung für Linux hinzugefügt . Dank der Escape-Sequenzen, die ich von der ursprünglichen Go-Quelle kopiert habe, habe ich auch die Textformatierung im Terminal hinzugefügt.



Bibliotheken



Da ich wenig Verständnis für Graphql hatte, hatte ich keine Ahnung, wo ich anfangen sollte. Eine Suche nach "graphql" auf code.dlang.org führte mich zu der entsprechenden Bibliothek mit dem treffenden Namen " graphqld ". Nach dem Studium schien es mir jedoch eher wie ein vibe.d-Plugin als wie ein echter Client, wenn überhaupt.



Nachdem ich Netzwerkanforderungen in Firefox untersucht hatte, stellte ich fest, dass ich für dieses Projekt einfach graphql-Anforderungen und -Transformationen simulieren kann, die ich mit einem HTTP-Client senden werde. Die Antworten sind nur JSON-Objekte, die ich mit den vom Paket std.json bereitgestellten Tools analysieren kann. Vor diesem Hintergrund habe ich mich auf die Suche nach HTTP-Clients gemacht und mich für Anfragen entschieden. Dies ist ein benutzerfreundlicher HTTP-Client, der jedoch vor allem einen bestimmten Reifegrad erreicht hat.



Ich habe die ausgehenden Anforderungen vom Netzwerkanalysator kopiert und in separate .graphql-Dateien eingefügt, die ich dann importiert und mit den entsprechenden Variablen gesendet habe. Der größte Teil der Funktionalität wurde in die GraphQLRequest-Struktur integriert, da ich je nach Projekt verschiedene Endpunkte und Konfigurationen einfügen wollte:



Quelle
struct GraphQLRequest
{
    string operationName;
    string query;
    JSONValue variables;
    Config configuration;

    JSONValue toJson()
    {
        return JSONValue([
            "operationName": JSONValue(operationName),
            "variables": variables,
            "query": JSONValue(query),
        ]);
    }

    string toString()
    {
        return toJson().toPrettyString();
    }

    Response send()
    {
        auto request = Request();
        request.addHeaders(["Authorization": configuration.get("token", "")]);
        return request.post(
            configuration.get("endpoint"),
            toString(),
            "application/json"
        );
    }
}




Hier ist ein Paketaustausch-Snippet. Der folgende Code behandelt die Authentifizierung:
struct Session
{
    Config configuration;

    void login(string username, string password)
    {
        auto request = createSession(username, password);
        auto response = request.send();
        response.throwOnFailure();
        string token = response.jsonBody
            ["data"].object
            ["createSession"].object
            ["token"].str;
        configuration.put("token", token);
    }

    GraphQLRequest createSession(string username, string password)
    {
        enum query = import("createSession.graphql").lineSplitter().join("\n");
        auto variables = SessionPayload(username, password).toJson();
        return GraphQLRequest("createSession", query, variables, configuration);
    }
}

struct SessionPayload
{
    string email;
    string password;

    //todo : make this a template mixin or something
    JSONValue toJson()
    {
        return JSONValue([
            "email": JSONValue(email),
            "password": JSONValue(password)
        ]);
    }

    string toString()
    {
        return toJson().toPrettyString();
    }
}




Spoiler-Alarm - das habe ich noch nie gemacht.



Alles geschieht so: Die Funktion main () erstellt aus den Befehlszeilenargumenten eine Konfigurationsstruktur und fügt sie in die Sitzungsstruktur ein, die die Funktionalität der Befehle login, logout und status implementiert. Die Methode createSession () erstellt eine graphQL-Abfrage, indem sie die eigentliche Abfrage aus der entsprechenden .graphql-Datei liest und die Variablen zusammen mit dieser übergibt. Ich wollte meinen Quellcode nicht mit graphQL-Mutationen und Abfragen verschmutzen, also habe ich sie in .graphql-Dateien verschoben, die ich dann zur Kompilierungszeit mit enum und import importiere. Letzteres erfordert ein Compiler-Flag, das auf stringImportPaths verweist (standardmäßig view /).



Bei der Methode login () besteht die einzige Verantwortung darin, die HTTP-Anforderung zu senden und die Antwort zu verarbeiten. In diesem Fall werden potenzielle Fehler behandelt, wenn auch nicht sehr sorgfältig. Anschließend wird das Token in einer Konfigurationsdatei gespeichert, die eigentlich nichts anderes als ein schönes JSON-Objekt ist.



Die throwOnFailure-Methode ist nicht Teil der Kernfunktionalität der Abfragebibliothek. Es ist eigentlich eine Hilfsfunktion, die eine schnelle und schmutzige Fehlerbehandlung durchführt:



void throwOnFailure(Response response)
{
    if(!response.isSuccessful || "errors" in response.jsonBody)
    {
        string[] errors = response.errors;
        throw new RequestException(errors.join("\n"));
    }
}


Da D UFCS unterstützt , kann die Syntax throwOnFailure (Antwort) als response.throwOnFailure () umgeschrieben werden. Dies erleichtert das Einbetten in andere Methodenaufrufe wie send (). Möglicherweise habe ich diese Funktionalität während des gesamten Projekts überbeansprucht.



Fehlerverarbeitung



D bevorzugt Ausnahmen bei der Fehlerbehandlung. Die Gründe werden hier ausführlich erläutert . Eines der Dinge, die ich liebe, ist, dass nicht behandelte Fehler irgendwann auftauchen, wenn sie nicht explizit behoben werden. Deshalb konnte ich mich der vereinfachten Fehlerbehandlung entziehen. Zum Beispiel in diesen Zeilen:



string token = response.jsonBody
    ["data"].object
    ["createSession"].object
    ["token"].str;
configuration.put("token", token);


Wenn der Antworttext kein Token oder eines der dazu führenden Objekte enthält, wird eine Ausnahme ausgelöst, die in der Hauptfunktion in die Luft sprudelt und dann vor dem Benutzer explodiert. Wenn ich Go verwenden würde, müsste ich bei jedem Schritt sehr vorsichtig mit Fehlern umgehen. Und ehrlich gesagt, da es ärgerlich ist, bei jedem Aufruf der Funktion zu schreiben, wenn err! = Null ist, wäre ich sehr versucht, den Fehler einfach zu ignorieren. Mein Verständnis von Go ist jedoch primitiv, und ich wäre nicht überrascht, wenn der Compiler Sie ankläfft, weil Sie nichts mit einer Fehlerrückgabe getan haben. Sie können mich also jederzeit korrigieren, wenn ich falsch liege.



Interessant war die Fehlerbehandlung im Roststil, wie im ursprünglichen Blogpost erläutert. Ich glaube nicht, dass es so etwas in der D-Standardbibliothek gibt, aber es gab Diskussionen darüber, dies als Bibliothek eines Drittanbieters zu implementieren.



Websockets



Ich möchte nur kurz darauf hinweisen, dass ich keine Websockets verwendet habe, um den Befehl watch zu implementieren. Ich habe versucht, den Websocket-Client von Vibe.d zu verwenden, aber er konnte nicht mit dem Hashtrack-Backend funktionieren, da die Verbindung immer wieder geschlossen wurde. Am Ende habe ich es zugunsten von Round Robin fallen lassen, obwohl es verpönt ist. Der Client hat gearbeitet, seit ich ihn mit einem anderen Webserver getestet habe, sodass ich möglicherweise in Zukunft darauf zurückkommen werde.



Kontinuierliche Integration



Für CI habe ich zwei Build-Jobs eingerichtet: einen regulären Zweig-Build und eine Master-Version, um sicherzustellen, dass optimierte Builds von Artefakten heruntergeladen werden.









Ca. Die Bilder zeigen die Montagezeit. Berücksichtigung des Ladens von Abhängigkeiten. Neuaufbau ohne Abhängigkeiten ~ 4s



Speicherverbrauch



Ich habe den Befehl / usr / bin / time -v ./hashtrack --list verwendet, um die Speichernutzung zu messen, wie im ursprünglichen Blog-Beitrag erläutert. Ich weiß nicht, ob die Speichernutzung von den Hashtags abhängt, denen der Benutzer folgt, aber hier sind die Ergebnisse eines D-Programms, das mit Dub Build -b Release kompiliert wurde:

Maximale Größe des residenten Satzes (KB): 10036

Maximale Größe des residenten Satzes (KB): 10164

Maximale Größe des residenten Satzes (KB): 9940

Maximale Größe des residenten Satzes (KB): 10060

Maximale Größe des residenten Satzes (KB): 10008


Nicht schlecht. Ich habe die Go- und Rust-Versionen mit meinem Hashtrack-Benutzer ausgeführt und die folgenden Ergebnisse erhalten:



Go erstellt mit go build -ldflags "-s -w":

Maximale Größe des residenten Satzes (KB): 13684

Maximale Größe des residenten Satzes ( KB): 13820 Maximale Größe des residenten Satzes (KB): 13904

Maximale Größe des residenten Satzes (KB): 13796

Maximale Größe des

residenten Satzes (KB): 13600

Rost zusammengestellt mit Frachtbau - Release:

Maximale Größe des residenten Satzes (KB): 9224

Maximale Größe des residenten Satzes (KB): 9192

Maximale Größe des residenten Satzes (KB): 9384

Maximale Größe des residenten Satzes (KB): 9132

Maximale Größe des residenten Satzes (KB): 9168
Update: Reddit- Benutzer skocznymroczny hat empfohlen , auch die LDC- und GDC-Compiler zu testen. Hier sind die Ergebnisse:

LDC 1.22 kompiliert von Dub Build -b Release --compiler = ldc2 (nach Hinzufügen von Farbausgabe und Getpass)

Maximale Größe des residenten Satzes (KB): 7816

Maximale Größe des residenten Satzes (KB): 7912

Maximale Größe des residenten Satzes (KB): 7804

Maximale Größe des residenten Satzes (KB): 7832

Maximale Größe des residenten Satzes (KB): 7804


D verfügt über eine Speicherbereinigung, unterstützt jedoch auch intelligente Zeiger und in jüngerer Zeit eine von Rust inspirierte experimentelle Speicherverwaltungsmethode . Ich bin mir nicht ganz sicher, wie gut sich diese Funktionen in die Standardbibliothek integrieren lassen, daher habe ich beschlossen, den GC den Speicher für mich verwalten zu lassen. Ich denke, die Ergebnisse sind ziemlich gut, wenn man bedenkt, dass ich beim Schreiben des Codes nicht an den Speicherverbrauch gedacht hatte.



Binärgröße



Rust, cargo build --release: 7.0M



D, dub build -b release: 5.7M



D, dub build -b release --compiler=ldc2: 2.4M



Go, go build: 7.1M



Go, go build -ldflags "-s -w": 5.0M


.. — , , . Windows dub build -b release 2 x64 ( 1.5M x86-mscoff) , Rust Ubuntu18 - openssl, ,





Ich denke, D ist eine zuverlässige Sprache zum Schreiben solcher Befehlszeilentools. Ich bin nicht sehr oft zu externen Abhängigkeiten gegangen, weil die Standardbibliothek das meiste enthielt, was ich brauchte. Dinge wie das Parsen von Befehlszeilenargumenten, die JSON-Verarbeitung, Unit-Tests und das Senden von HTTP-Anforderungen (mit cURL ) sind in der Standardbibliothek verfügbar. Wenn in der Standardbibliothek nicht das vorhanden ist, was Sie benötigen, gibt es Pakete von Drittanbietern, aber ich denke, in diesem Bereich gibt es noch Verbesserungspotenzial. Auf der anderen Seite, wenn Ihre NIH-Mentalität hier nicht erfunden ist oder wenn Sie als Open-Source-Entwickler mühelos etwas bewirken möchten, werden Sie das D-Ökosystem auf jeden Fall lieben.



Gründe, warum ich D verwenden würde



  • Ja



All Articles