wc bis D: 712 Zeichen ohne einen einzigen Zweig

Nachdem ich " Beat With a 80-Line Program on Haskell" gelesen hatte , das ich in den Hacker News gefunden hatte , entschied ich, dass D besser sein könnte. Und ich schrieb wc in D.

Note.per. Ich schlug vor, den obigen Artikel zu übersetzen0xd34df00d, aber er zog es vor, basierend auf seinem "Winning With Twenty Lines of Haskell: Writing Your Wc" zu machen . Und jetzt vermehren sich die Artikel wie Aufgüsse "geprägte Münzen".

Programm


Besteht aus einer Datei - 34 Zeilen und 712 Zeichen.

Quellcode
import std.stdio : writefln, File;
import std.algorithm : map, fold, splitter;
import std.range : walkLength;
import std.typecons : Yes;
import std.uni : byCodePoint;

struct Line {
	size_t chars;
	size_t words;
}

struct Output {
	size_t lines;
	size_t words;
	size_t chars;
}

Output combine(Output a, Line b) pure nothrow {
	return Output(a.lines + 1, a.words + b.words, a.chars + b.chars);
}

Line toLine(char[] l) pure {
	return Line(l.byCodePoint.walkLength, l.splitter.walkLength);
}

void main(string[] args) {
	auto f = File(args[1]);
	Output o = f
		.byLine(Yes.keepTerminator)
		.map!(l => toLine(l))
		.fold!(combine)(Output(0, 0, 0));

	writefln!"%u %u %u %s"(o.lines, o.words, o.chars, args[1]);
}


Natürlich benutzt er Phobos, die D-Standardbibliothek , aber warum nicht? Phobos ist wunderschön und wird mit jedem D-Compiler geliefert. Das Programm selbst enthält keine if-Anweisungen. Und in der WC-Implementierung auf Haskell werden mehrere if verwendet. Das D-Programm enthält neben dem Hauptprogramm drei weitere kleine Funktionen. Ich könnte leicht alle Funktionen in eine Bereichskette einordnen, aber dann würde es wahrscheinlich mehr als 80 Zeichen pro Zeile geben. Dies ist das Grundprinzip des Codestils.

Performance


Ist wc auf D schneller als coreutils wc? Nein, aber ich habe 15 Minuten gebraucht, um meine Version zu schreiben (ich musste nach walkLength suchen, weil ich den Namen der Funktion vergessen habe).
DatendateiLinienByteCoreutilshaskellD.
app.d469063,5 ms ± 1,9 ms39,6 ms ± 7,8 ms8,9 ms ± 2,1 ms
big.txt86264k4,7 ms ± 2,0 ms39,6 ms ± 7,8 ms9,8 ms ± 2,1 ms
vbig.txt1,7 m96M658,6 ms ± 24,5 ms226,4 ms ± 29,5 ms1,102 s ± 0,022 s
vbig2.txt12,1M671M4,4 s ± 0,058 s1,1 s ± 0,039 s7,4 s ± 0,085 s

Erinnerung:
DatendateiCoreutilshaskellD.
app.d2052K7228K7708K
big.txt2112K7512K7616K
vbig.txt2288K42620K7712K
vbig2.txt2360K50860K7736K
wc auf Haskell schneller? Natürlich für große Dateien, aber es wird Multithreading verwendet. Bei kleinen Dateien gewinnt GNU coreutils immer noch. Zu diesem Zeitpunkt ist meine Version höchstwahrscheinlich durch E / A begrenzt, und auf jeden Fall ist sie ziemlich schnell.

Ich werde nicht argumentieren, dass eine Sprache schneller ist als eine andere. Wenn Sie Zeit damit verbringen, den Mikro-Benchmark zu optimieren, werden Sie höchstwahrscheinlich gewinnen. Aber nicht im wirklichen Leben. Aber ich werde argumentieren, dass die funktionale Programmierung in D fast die FP in Haskell einholt.

Ein bisschen über die Reichweite in D.


range ist eine Abstraktion, die iteriert werden kann, ohne die zugrunde liegende Sammlung zu berühren (falls vorhanden). Technisch gesehen kann range eine Struktur oder Klasse sein, die eine oder mehrere Range-Schnittstellen implementiert. Die einfachste Form, InputRange, erfordert eine Funktion

void popFront();

und zwei Mitglieder oder Eigenschaften

T front;
bool empty;

T ist ein generischer Elementtyp, über den der Bereich iteriert.

D-Bereiche sind bestimmte Typen. Wenn der Bereich in die foreach-Anweisung fällt, führt der Compiler eine kleine Änderung durch.

foreach (e; range) { ... }

verwandelt sich in

for (auto __r = range; !__r.empty; __r.popFront()) {
    auto e = __r.front;
    ...
}

auto e = berechnet den Typ und entspricht T e =.
In diesem Sinne ist es einfach, einen Bereich zu erstellen, der von foreach verwendet werden kann.

struct Iota {
	int front;
	int end;

	@property bool empty() const {
		return this.front == this.end;
	}

	void popFront() {
		++this.front;
	}
}

unittest {
	import std.stdio;
	foreach(it; Iota(0, 10)) {
		writeln(it);
	}
}

Iota ist ein sehr einfaches Beispiel für Reichweite. Es fungiert als Generator ohne die zugrunde liegende Sammlung. Es durchläuft ganze Zahlen von Anfang bis Ende in Schritten von eins. Dieses Snippet enthüllt ein bisschen D-Syntax.

@property bool empty() const {

Mit dem @ property- Attribut können Sie die leere Funktion wie eine Mitgliedsvariable verwenden (Aufruf einer Funktion ohne Klammern). Das const- Attribut am Ende bedeutet, dass wir die Instanzdaten, für die wir leer aufrufen, nicht ändern . Der integrierte Komponententest gibt Zahlen von 0 bis 10 aus.

Ein weiteres kleines Merkmal ist das Fehlen eines expliziten Konstruktors. Die Iota-Struktur enthält zwei int-Mitgliedsvariablen. In der foreach-Anweisung im Test erstellen wir eine Iota-Instanz, als hätte sie einen Konstruktor, der zwei Ints akzeptiert. Dies ist ein strukturelles Literal. Wenn der Compiler D dies sieht, die Struktur jedoch keinen entsprechenden Konstruktor hat, werden gemäß der Reihenfolge der Deklaration von Variablen den Mitgliedern der Struktur int zugewiesen.

Die Beziehung zwischen den drei Mitgliedern ist einfach. Wenn leer falsch ist, gibt front nach der Iteration nach dem Aufruf von popFront garantiert ein neues Element zurück. Nach dem Aufruf von popFront kann sich der leere Wert ändern. Wenn dies zutrifft, bedeutet dies, dass keine Elemente mehr iteriert werden müssen und weitere Aufrufe an front ungültig sind. Laut InputRange-Dokumentation :

  • front kann genau dann korrekt berechnet werden, wenn leer oder falsch zurückgegeben wird.
  • front kann mehrmals berechnet werden, ohne popFront aufzurufen oder den Bereich oder die zugrunde liegenden Daten auf andere Weise zu mutieren, und es wird für jede Berechnung das gleiche Ergebnis erzielt.

Die Verwendung von foreach-Ausdrücken oder Schleifen ist nicht sehr funktional. Angenommen, wir möchten alle ungeraden Zahlen für Iota herausfiltern. Wir könnten ein Wenn in einen Foreach-Block stecken, aber das würde es nur noch schlimmer machen. Es wäre besser, wenn wir einen Bereich hätten, der einen Bereich annimmt, und ein Prädikat, das entscheidet, ob ein Element geeignet ist oder nicht.

struct Filter {
	Iota input;
	bool function(int) predicate;

	this(Iota input, bool function(int) predicate) {
		this.input = input;
		this.predicate = predicate;
		this.testAndIterate();
	}

	void testAndIterate() {
		while(!this.input.empty
				&& !this.predicate(this.input.front))
		{
			this.input.popFront();
		}
	}

	void popFront() {
		this.input.popFront();
		this.testAndIterate();
	}

	@property int front() {
		return this.input.front;
	}

	@property bool empty() const {
		return this.input.empty;
	}
}

bool isEven(int a) {
	return a % 2 == 0;
}

unittest {
	foreach(it; Filter(Iota(0,10), &isEven)) {
		writeln(it);
	}
}

Der Filter ist wieder sehr einfach: Iota und ein Funktionszeiger werden benötigt. Beim Erstellen des Filters rufen wir testAndIterate auf, das Elemente von Iota übernimmt, bis sie leer sind oder das Prädikat false zurückgibt. Die Idee ist, dass das Prädikat entscheidet, was gefiltert und was verlassen wird. Die vorderen und leeren Eigenschaften werden einfach in Iota übersetzt. Das einzige, was den Job tatsächlich macht, ist popFront. Es gibt das aktuelle Element zurück und ruft testAndIterate auf. Das ist alles. Dies ist eine Filterimplementierung.

Natürlich hat testAndIterate eine while-Schleife, aber das Umschreiben mit Rekursion ist meiner Meinung nach einfach nur dumm. Was D großartig macht, ist, dass Sie für jede Aufgabe die entsprechende Methode verwenden können. Funktionale Programmierung ist gut und wird oft zur Schau gestellt, manchmal aber auch nicht. Wenn ein kleiner Inline-Assembler benötigt wird oder mehr Spaß macht, verwenden Sie.

Das Aufrufen von Filter sieht immer noch nicht sehr gut aus. Angenommen, Sie sind es gewohnt, von links nach rechts zu lesen, wird Filter vor Iota angezeigt, auch wenn er nach Iota ausgeführt wird. D stellt keine Pipe-Anweisung bereit, verwendet jedoch die Unified Function Call Syntax (UFCS). Wenn ein Ausdruck implizit in den ersten Parameter einer Funktion konvertiert werden kann, kann die Funktion so aufgerufen werden, als wäre sie eine Mitgliedsfunktion dieses Ausdrucks. Das ist ziemlich kompliziert, verstehe ich. Ein Beispiel hilft:

string foo(string a) {
	return a ~ "World";
}

unittest {
	string a = foo("Hello ");
	string b = "Hello ".foo();
	assert(a == b);
}

Das obige Beispiel zeigt zwei Aufrufe der Funktion foo. Wie Assert impliziert, sind beide Aufrufe gleichwertig. Was bedeutet das für unser Iota-Filter-Beispiel? Mit UFCS können wir den Komponententest folgendermaßen umschreiben:

unittest {
	foreach(it; Iota(1,10).Filter(&isEven)) {
		writeln(it);
	}
}

Die Map / Transform-Implementierung für Range sollte nun für jeden Leser zugänglich sein. Natürlich kann ein Filter mithilfe von Vorlagen abstrakter gestaltet werden, aber dies sind nur Details, konzeptionell nichts Neues.

Natürlich gibt es verschiedene Arten von Bereichen, zum Beispiel bidirektional. Ratet mal, welche Möglichkeiten dies für euch bietet. Ein kurzer Tipp: Es gibt zwei neue Grundelemente im bidirektionalen Bereich, die als back und popBack bezeichnet werden. Es gibt andere Arten von Bereichen, aber nachdem Sie den oben gezeigten Eingabebereich zweimal verstanden haben, erkennen Sie sie fast alle.

PS Implementieren Sie aus Gründen der Übersichtlichkeit keinen eigenen Filter, keine eigene Karte oder Falte. Die Phobos D Standardbibliothek bietet alles, was Sie brauchen. Schauen Sie sich std.algorithm und std.range an .

Über den Autor


Robert Schadeck erwarb einen Master in Informatik an der Universität Oldenburg. Seine Masterarbeit trug den Titel "DMCD - Distributed Multithreaded D Compiler mit Caching", wo er an der Erstellung eines D-Compilers von Grund auf arbeitete. Von 2012 bis 2018 war er Doktorand in Informatik. an der Universität Oldenburg. Seine Dissertation befasst sich mit Quorumsystemen in Verbindung mit Grafiken. Seit 2018 verwendet er D gerne in seiner täglichen Arbeit für Symmetry Investments.

Über Symmetry Investments


Symmetry Investments ist eine globale Investmentgesellschaft mit Niederlassungen in Hongkong, Singapur und London. Wir sind seit 2014 im Geschäft, nachdem wir uns erfolgreich von einem großen New Yorker Hedgefonds getrennt haben.

Bei Symmetry sind wir bestrebt, angemessene Risiken einzugehen, um Wert für unsere Kunden, Partner und Mitarbeiter zu schaffen. Wir profitieren von unserer Fähigkeit, Win-Win-Verträge zu generieren - im weitesten Sinne des Wortes. Win-Win ist unser grundlegendes ethisches und strategisches Prinzip. Durch die Generierung von Win-Win-Optionen können wir einzigartige Lösungen schaffen, die Perspektiven kombinieren, die normalerweise als inkompatibel oder gegensätzlich empfunden werden, und das Beste abdecken, was jede der Parteien bieten kann. Wir integrieren die Fixed Income Arbitration wieder in globale Makrostrategien. Wir erfinden und entwickeln eine Technologie, die sich auf das Potenzial der Mensch-Maschine-Integration konzentriert. Wir schaffen Systeme, in denen Maschinen das tun, was sie am besten können, und Menschen dabei unterstützendamit sie das tun, was sie am besten können. Wir schaffen eine Meritokratie, die auf Kooperation basiert: eine Kultur, in der der individuelle Beitrag sowohl persönlichen als auch kollektiven Zielen dient - und entsprechend belohnt wird. Wir legen Wert auf Eigenverantwortung und Teamgeist der Zusammenarbeit, Selbstverwirklichung und Gemeinschaft.

Die Mitarbeiter von Symmetry Investments sind seit 2014 aktive Mitglieder von Community D. Wir haben die Entwicklung von Excel-D-, Dpp-, Autowrap-, Libmir- und anderen Projekten gesponsert. Wir haben Symmetry Autumn of Code im Jahr 2018 gestartet und die DConf 2019 in London veranstaltet.

Ca. Da der Übersetzer wie üblich betrunken war, kann ich nicht für die Richtigkeit einiger menschlicher Begriffe bürgen und verstehe im letzten Abschnitt nichts. Senden Sie also Korrekturen in der persönlichen Kommunikation.

All Articles