wc a D: 712 caracteres sin una sola rama

Después de leer " Batir con un programa de 80 líneas en Haskell", que encontré en Hacker News , decidí que D podría ser mejor. Y escribí wc en D.

Note.per. Sugerí traducir el artículo anterior0xd34df00d, pero prefirió hacer basado en su "Ganar con veinte líneas de Haskell: Escribiendo su Wc" . Y ahora los artículos se multiplican como repeticiones de "monedas acuñadas".

Programa


Consta de un archivo: 34 líneas y 712 caracteres.

Código fuente
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]);
}


Por supuesto, usa Phobos, la biblioteca estándar D , pero ¿por qué no? Phobos es hermoso y viene con todos los compiladores D. El programa en sí no contiene ninguna declaración if. Y en la implementación de wc en Haskell, se usan varios si. El programa D, además del principal, contiene tres funciones más pequeñas. Podría poner fácilmente toda la funcionalidad en una cadena de rango, pero probablemente superaría los 80 caracteres por línea. Este es el principio básico del estilo de código.

Actuación


¿Es wc en D más rápido que coreutils wc? No, pero me llevó 15 minutos escribir mi versión (tuve que buscar walkLength porque olvidé el nombre de la función).
archivo de datoslíneasbytecoreutilsHaskellre
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 millones96 millones658.6ms ± 24.5ms226,4 ms ± 29,5 ms1.102 s ± 0.022 s
vbig2.txt12,1 millones671 millones4,4 s ± 0,058 s1.1 s ± 0.039 s7,4 s ± 0,085 s

Memoria:
archivo de datoscoreutilsHaskellre
app.d2052K7228K7708K
big.txt2112K7512K7616K
vbig.txt2288K42620K7712K
vbig2.txt2360K50860K7736K
WC en Haskell más rápido? Para archivos grandes, sin duda, pero utiliza subprocesos múltiples. Para archivos pequeños, GNU coreutils aún gana. En esta etapa, lo más probable es que mi versión esté limitada por IO y, en cualquier caso, es bastante rápida.

No argumentaré que un idioma es más rápido que otro. Si pasa tiempo optimizando el micro punto de referencia, lo más probable es que gane. Pero no en la vida real. Pero argumentaré que la programación funcional en D casi alcanza al FP en Haskell.

Un poco sobre el rango en D


El rango es una abstracción que se puede iterar sin tocar la colección subyacente (si existe). Técnicamente, el rango puede ser una estructura o clase que implementa una o más interfaces de rango. La forma más simple, InputRange, requiere una función

void popFront();

y dos miembros o propiedades

T front;
bool empty;

T es un tipo genérico de elemento cuyo rango se repite.

Los rangos D son tipos específicos. Cuando el rango cae en la instrucción foreach, el compilador realiza una pequeña modificación.

foreach (e; range) { ... }

se convierte en

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

auto e = calcula el tipo y es equivalente a T e =.
Con esto en mente, es fácil crear un rango que pueda ser utilizado por 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 es un ejemplo muy simple de rango. Funciona como un generador, sin tener la colección subyacente. Se itera a través de enteros de principio a fin en incrementos de uno. Este fragmento revela un poco de sintaxis D.

@property bool empty() const {

El atributo @ property le permite usar la función vacía de la misma manera que una variable miembro (llamando a una función sin paréntesis). El atributo const al final significa que no estamos modificando los datos de instancia para los que llamamos vacío . La prueba de unidad incorporada imprime números del 0 al 10.

Otra característica pequeña es la falta de un constructor explícito. La estructura Iota tiene dos variables miembro int. En la declaración foreach en la prueba, creamos una instancia de Iota como si tuviera un constructor que aceptara dos entradas. Este es un literal estructural. Cuando el compilador D ve esto, pero la estructura no tiene un constructor correspondiente, entonces de acuerdo con el orden de declaración de variables, los miembros de la estructura se asignarán int.

La relación entre los tres miembros es simple. Si vacío es falso, se garantiza que front devolverá un nuevo elemento después de la iteración después de llamar a popFront. Después de llamar a popFront, el valor vacío podría cambiar. Si esto es cierto, significa que no hay más elementos para iterar y las llamadas al frente adicionales no son válidas. De acuerdo con la documentación de InputRange :

  • el frente se puede calcular correctamente si y solo si el vacío devuelve o devolvería falso.
  • front se puede calcular varias veces sin llamar a popFront o al rango mutante o datos subyacentes, y da el mismo resultado para cada cálculo.

Usar expresiones foreach, o bucles, no es muy funcional. Supongamos que queremos filtrar todos los números impares para Iota. Podríamos poner un if dentro de un bloque foreach, pero eso solo lo empeoraría. Sería mejor si tuviéramos un rango que tome un rango y un predicado que decida si un elemento es adecuado o no.

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

El filtro es nuevamente muy simple: se necesitan Iota y un puntero de función. Al construir el filtro, llamamos a testAndIterate, que toma los elementos de Iota hasta que esté vacío o el predicado devuelva falso. La idea es que el predicado decida qué filtrar y qué dejar. Las propiedades frontal y vacía simplemente se traducen a Iota. Lo único que realmente hace el trabajo es popFront. Devuelve el elemento actual y llama a testAndIterate. Eso es todo. Esta es una implementación de filtro.

Por supuesto, testAndIterate tiene un ciclo while, pero reescribirlo usando recursión, en mi opinión, es simplemente estúpido. Lo que hace que D sea genial es que puedes usar el método apropiado para cada tarea. La programación funcional es buena y, a menudo, hace alarde, pero a veces no lo es. Si se necesita un pequeño ensamblador en línea o más agradable, úselo.

Llamar a Filtro todavía no se ve muy bien. Suponiendo que está acostumbrado a leer de izquierda a derecha, Filter aparece antes de Iota, incluso si se ejecuta después de Iota. D no proporciona una declaración de canalización, pero utiliza la sintaxis de llamada de función unificada (UFCS). Si una expresión se puede convertir implícitamente en el primer parámetro de una función, entonces la función se puede llamar como si fuera una función miembro de esta expresión. Esto es bastante complicado, entiendo. Un ejemplo ayudará:

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

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

El ejemplo anterior muestra dos llamadas a la función foo. Como implica afirmar, ambas llamadas son equivalentes. ¿Qué significa esto para nuestro ejemplo de filtro Iota? UFCS nos permite reescribir la prueba unitaria de esta manera:

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

La implementación del mapa / transformación para el rango ahora debería ser accesible para todos los lectores. Por supuesto, un filtro puede hacerse más abstracto usando plantillas, pero estos son solo detalles, nada conceptualmente nuevo.

Por supuesto, hay diferentes tipos de rango, por ejemplo, bidireccional. Adivina qué oportunidades te da esto. Un consejo rápido: hay dos primitivas nuevas en el rango bidireccional llamadas back y popBack. Existen otros tipos de rango, pero después de comprender el rango de entrada que se muestra arriba dos veces, casi todos los reconocen.

PD: Para mayor claridad, no implemente su propio filtro, mapa o pliegue; La biblioteca estándar Phobos D tiene todo lo que necesita. Eche un vistazo a std.algorithm y std.range .

Sobre el Autor


Robert Schadeck obtuvo una maestría en ciencias de la computación de la Universidad de Oldenburg. Su tesis de maestría se tituló "DMCD - Compilador D multiproceso distribuido con almacenamiento en caché", donde trabajó en la creación de un compilador D desde cero. Fue un estudiante graduado en ciencias de la computación en 2012-2018. en la universidad de Oldenburg. Su tesis doctoral se centra en los sistemas de quórum junto con los gráficos. Desde 2018, se complace en utilizar D en su trabajo diario, trabajando para Symmetry Investments.

Acerca de las inversiones en simetría


Symmetry Investments es una compañía de inversión global con oficinas en Hong Kong, Singapur y Londres. Hemos estado haciendo negocios desde 2014 después de separarnos con éxito de un importante fondo de cobertura de Nueva York.

En Symmetry, nos esforzamos por asumir riesgos de manera razonable para crear valor para nuestros clientes, socios y empleados. Nos beneficiamos de nuestra capacidad de generar contratos de beneficio mutuo, en el sentido más amplio de la palabra. Win-Win es nuestro principio ético y estratégico fundamental. Al generar opciones de ganar-ganar, podemos crear soluciones únicas que combinan perspectivas que generalmente se perciben como incompatibles u opuestas, y que cubren todo lo mejor que cada una de las partes puede ofrecer. Estamos reintegrando el arbitraje de renta fija con estrategias macro globales. Inventamos y desarrollamos una tecnología que se centra en el potencial de la integración hombre-máquina. Creamos sistemas en los que las máquinas hacen lo que hacen mejor, apoyando a las personas enpara que hagan lo que hacen mejor. Creamos una meritocracia basada en la cooperación: una cultura en la que la contribución individual sirve tanto para objetivos personales como colectivos, y se recompensa en consecuencia. Valoramos tanto un sentido de propiedad como un espíritu de equipo de cooperación, autorrealización y comunidad.

Las personas de Symmetry Investments han sido miembros activos de la Comunidad D desde 2014. Patrocinamos el desarrollo de excel-d, dpp, autowrap, libmir y otros proyectos. Lanzamos Symmetry Autumn of Code en 2018 y alojamos DConf 2019 en Londres.

Aprox. Como el traductor, como de costumbre, estaba borracho, no puedo garantizar la exactitud de algunos términos humanos, y no entiendo nada en la última sección. Entonces envíe correcciones en comunicaciones personales.

All Articles