wc à D: 712 caractères sans une seule branche

Après avoir lu « Beat With an 80-Line Program at Haskell», que j'ai trouvé sur Hacker News , j'ai décidé que D pourrait être mieux. Et j'ai écrit wc dans D.

Note.per. J'ai suggéré de traduire l'article ci-dessus0xd34df00d, mais il a préféré faire sur la base de son «Winning With Twenty Lines of Haskell: Writing Your Wc» . Et maintenant, les articles se multiplient comme des remaniements de «pièces frappées».

Programme


Se compose d'un fichier - 34 lignes et 712 caractères.

Code source
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]);
}


Bien sûr, il utilise Phobos, la bibliothèque standard D , mais pourquoi pas? Phobos est magnifique et est fourni avec tous les compilateurs D. Le programme lui-même ne contient aucune instruction if. Et dans l'implémentation wc sur Haskell, plusieurs si sont utilisés. Le programme D, en plus du programme principal, contient trois autres fonctions minuscules. Je pourrais facilement mettre toutes les fonctionnalités dans une chaîne de gamme, mais cela dépasserait probablement 80 caractères par ligne. C'est le principe de base du style de code.

Performance


Le wc sur D est-il plus rapide que le coreutils wc? Non, mais il m'a fallu 15 minutes pour écrire ma version (j'ai dû chercher walkLength car j'avais oublié le nom de la fonction).
fichier de donnéeslignesoctetcoreutilshaskell
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,1 millions671M4,4 s ± 0,058 s1,1 s ± 0,039 s7,4 s ± 0,085 s

Mémoire:
fichier de donnéescoreutilshaskell
app.d2052K7228K7708K
big.txt2112K7512K7616K
vbig.txt2288K42620K7712K
vbig2.txt2360K50860K7736K
wc sur Haskell plus vite? Pour les gros fichiers, certes, mais il utilise le multithreading. Pour les petits fichiers, GNU coreutils gagne toujours. À ce stade, ma version est très probablement limitée par IO, et en tout cas, elle est assez rapide.

Je ne dirai pas qu'une langue est plus rapide qu'une autre. Si vous passez du temps à optimiser le micro-benchmark, vous gagnerez très probablement. Mais pas dans la vraie vie. Mais je dirai que la programmation fonctionnelle en D rattrape presque le FP de Haskell.

Un peu sur la portée en ré


range est une abstraction qui peut être itérée sans toucher à la collection sous-jacente (le cas échéant). Techniquement, la plage peut être une structure ou une classe qui implémente une ou plusieurs interfaces de plage. La forme la plus simple, InputRange, nécessite une fonction

void popFront();

et deux membres ou propriétés

T front;
bool empty;

T est un type d'élément générique que la plage parcourt.

Les plages D sont des types spécifiques. Lorsque la plage tombe dans l'instruction foreach, le compilateur effectue une petite modification.

foreach (e; range) { ... }

se transforme en

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

auto e = calcule le type et est équivalent à T e =.
Dans cet esprit, il est facile de créer une gamme pouvant être utilisée par foreach.

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 est un exemple très simple de gamme. Il fonctionne comme un générateur, sans avoir la collection sous-jacente. Il parcourt les entiers du début à la fin par incréments de un. Cet extrait révèle un peu de syntaxe D.

@property bool empty() const {

L'attribut @ property vous permet d'utiliser la fonction vide de la même manière qu'une variable membre (appel d'une fonction sans parenthèses). L'attribut const à la fin signifie que nous ne modifions pas les données d'instance pour lesquelles nous appelons vide . Le test unitaire intégré imprime les nombres de 0 à 10.

Une autre petite caractéristique est le manque d'un constructeur explicite. La structure Iota a deux variables membres int. Dans l'instruction foreach du test, nous créons une instance Iota comme si elle avait un constructeur qui accepte deux entrées. Il s'agit d'un littéral structurel. Lorsque le compilateur D le voit, mais que la structure n'a pas de constructeur correspondant, alors conformément à l'ordre de déclaration des variables - les membres de la structure seront affectés int.

La relation entre les trois membres est simple. Si vide est faux, front est garanti de retourner un nouvel élément après l'itération après avoir appelé popFront. Après avoir appelé popFront, la valeur vide peut changer. Si cela est vrai, cela signifie qu'il n'y a plus d'éléments à réitérer et que les appels ultérieurs au front ne sont pas valides. Selon la documentation InputRange :

  • front peut être correctement calculé si et seulement si vide retourne ou retournerait faux.
  • front peut être calculé plusieurs fois sans appeler popFront ou autrement muter la plage ou les données sous-jacentes, et il donne le même résultat pour chaque calcul.

L'utilisation d'expressions ou de boucles foreach n'est pas très fonctionnelle. Supposons que nous voulons filtrer tous les nombres impairs pour Iota. Nous pourrions mettre un if à l'intérieur d'un bloc foreach, mais cela ne ferait qu'empirer les choses. Ce serait mieux si nous avions une plage qui prend une plage et un prédicat qui décide si un élément convient ou non.

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);
	}
}

Le filtre est à nouveau très simple: Iota et un pointeur de fonction sont nécessaires. Lors de la construction du filtre, nous appelons testAndIterate, qui prend des éléments de Iota jusqu'à ce qu'il soit vide ou que le prédicat renvoie faux. L'idée est que le prédicat décide quoi filtrer et quoi laisser. Les propriétés avant et vides sont simplement traduites en Iota. La seule chose qui fait le travail est popFront. Il renvoie l'élément actuel et appelle testAndIterate. C'est tout. Il s'agit d'une implémentation de filtre.

Bien sûr, testAndIterate a une boucle while, mais la réécrire en utilisant la récursivité, à mon avis, est tout simplement stupide. Ce qui rend D super, c'est que vous pouvez utiliser la méthode appropriée pour chaque tâche. La programmation fonctionnelle est bonne et fait souvent étalage, mais parfois elle ne l'est pas. Si un petit assembleur en ligne est nécessaire ou plus agréable, utilisez.

L'appel du filtre ne semble toujours pas très bon. En supposant que vous ayez l'habitude de lire de gauche à droite, le filtre apparaît avant Iota, même s'il s'exécute après Iota. D ne fournit pas d'instruction de canal, mais utilise la syntaxe d'appel de fonction unifiée (UFCS). Si une expression peut être implicitement convertie en le premier paramètre d'une fonction, alors la fonction peut être appelée comme si elle était une fonction membre de cette expression. C'est assez compliqué, je comprends. Un exemple aidera:

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

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

L'exemple ci-dessus montre deux appels à la fonction foo. Comme l'affirme l'assert, les deux appels sont équivalents. Qu'est-ce que cela signifie pour notre exemple de filtre Iota? UFCS nous permet de réécrire le test unitaire comme ceci:

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

L'implémentation map / transform pour range devrait désormais être accessible à tous les lecteurs. Bien sûr, un filtre peut être rendu plus abstrait à l'aide de modèles, mais ce ne sont que des détails, rien de nouveau conceptuellement.

Bien sûr, il existe différents types de plages, par exemple bidirectionnelles. Devinez quelles opportunités cela vous offre. Un petit conseil: il y a deux nouvelles primitives dans la plage bidirectionnelle appelées back et popBack. Il existe d'autres types de plage, mais une fois que vous avez compris la plage d'entrée indiquée ci-dessus deux fois, vous les reconnaissez presque tous.

PS Pour plus de clarté, n'implémentez pas votre propre filtre, carte ou pli; la bibliothèque standard Phobos D a tout ce dont vous avez besoin. Jetez un œil à std.algorithm et std.range .

A propos de l'auteur


Robert Schadeck a obtenu une maîtrise en informatique de l'Université d'Oldenburg. Son mémoire de maîtrise était intitulé «DMCD - Compilateur D multithread distribué avec mise en cache», où il a travaillé à la création d'un compilateur D à partir de zéro. Il était étudiant diplômé en informatique en 2012-2018. à l'Université d'Oldenburg. Sa thèse de doctorat se concentre sur les systèmes de collège en conjonction avec des graphiques. Depuis 2018, il est heureux d'utiliser D dans son travail quotidien, travaillant pour Symmetry Investments.

À propos d'Investissements Symétrie


Symmetry Investments est une société d'investissement mondiale avec des bureaux à Hong Kong, Singapour et Londres. Nous exerçons nos activités depuis 2014 après nous être séparés avec succès d'un important hedge fund new-yorkais.

Chez Symmetry, nous nous efforçons de prendre des risques raisonnables pour créer de la valeur pour nos clients, partenaires et employés. Nous bénéficions de notre capacité à générer des contrats gagnant-gagnant - au sens large du terme. Win-Win est notre principe éthique et stratégique fondamental. En générant des options gagnant-gagnant, nous pouvons créer des solutions uniques qui combinent des perspectives généralement perçues comme incompatibles ou opposées, et couvrant tout le meilleur que chacune des parties peut offrir. Nous réintégrons l'arbitrage des titres à revenu fixe dans les macro-stratégies mondiales. Nous inventons et développons une technologie qui se concentre sur le potentiel d'intégration homme-machine. Nous créons des systèmes dans lesquels les machines font ce qu'elles font le mieux, en aidant lesafin qu'ils fassent ce qu'ils font de mieux. Nous créons une méritocratie basée sur la collaboration: une culture dans laquelle la contribution individuelle sert des objectifs personnels et collectifs - et est récompensée en conséquence. Nous apprécions à la fois un sentiment d'appartenance et un esprit d'équipe de coopération, de réalisation de soi et de communauté.

Les gens d'Investissements Symétrie sont membres actifs de la communauté D depuis 2014. Nous avons parrainé le développement d'Excel-D, Dpp, Autowrap, Libmir et d'autres projets. Nous avons lancé Symmetry Autumn of Code en 2018 et hébergé DConf 2019 à Londres.

Environ. Comme le traducteur, comme d'habitude, était ivre, je ne peux pas garantir l'exactitude de certains termes humains, et je ne comprends rien dans la dernière section. Envoyez donc les corrections dans les communications personnelles.

All Articles