wc para D: 712 caracteres sem uma única ramificação

Depois de ler “ Beat With a 80-Line Program on Haskell”, que encontrei no Hacker News , decidi que D poderia ser melhor. E eu escrevi wc em D.

Note.per. Eu sugeri traduzir o artigo acima0xd34df00d, mas ele preferiu fazer com base em seu "Vencendo com vinte linhas de Haskell: Writing Your Wc" . E agora os artigos estão se multiplicando como repetições "moeda cunhada".

Programa


Consiste em um arquivo - 34 linhas e 712 caracteres.

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


Claro, ele usa Phobos, a biblioteca padrão D , mas por que não? Phobos é bonito e vem com todo compilador D. O programa em si não contém nenhuma instrução if. E na implementação wc no Haskell, vários se forem usados. O programa D, além do principal, contém mais três pequenas funções. Eu poderia facilmente colocar toda a funcionalidade em uma cadeia de intervalo, mas provavelmente excederia 80 caracteres por linha. Este é o princípio básico do estilo de código.

atuação


O wc em D é mais rápido que o coreutils wc? Não, mas levei 15 minutos para escrever minha versão (eu tive que procurar walkLength porque esqueci o nome da função).
arquivo de dadoslinhasbytecoreutilshaskellD
app.d46.9063,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.7M96 milhões658.6ms ± 24.5ms226,4 ms ± 29,5 ms1,102 s ± 0,022 s
vbig2.txt12,1 milhões671M4,4 s ± 0,058 s1,1 s ± 0,039 s7,4 s ± 0,085 s

Memória:
arquivo de dadoscoreutilshaskellD
app.d2052K7228K7708K
big.txt2112K7512K7616K
vbig.txt2288K42620K7712K
vbig2.txt2360K50860K7736K
wc em Haskell mais rápido? Para arquivos grandes, certamente, mas usa multithreading. Para arquivos pequenos, o GNU coreutils ainda vence. Nesta fase, minha versão provavelmente é limitada pelo IO e, em qualquer caso, é bastante rápida.

Não vou argumentar que um idioma é mais rápido que outro. Se você gastar tempo otimizando o micro benchmark, provavelmente ganhará. Mas não na vida real. Mas argumentarei que a programação funcional em D quase alcança o FP em Haskell.

Um pouco sobre o alcance em D


range é uma abstração que pode ser iterada sem tocar na coleção subjacente (se houver). Tecnicamente, o intervalo pode ser uma estrutura ou classe que implementa uma ou mais interfaces de intervalo. A forma mais simples, InputRange, requer uma função

void popFront();

e dois membros ou propriedades

T front;
bool empty;

T é um tipo genérico de elemento cujo intervalo é repetido.

Os intervalos D são tipos específicos. Quando o intervalo cai na instrução foreach, o compilador executa uma pequena modificação.

foreach (e; range) { ... }

torna-se em

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

auto e = calcula o tipo e é equivalente a T e =.
Com isso em mente, é fácil criar um intervalo que possa ser usado pelo 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 é um exemplo muito simples de alcance. Funciona como um gerador, sem a coleção subjacente. Ele percorre números inteiros do início ao fim em incrementos de um. Este trecho revela um pouco da sintaxe D.

@property bool empty() const {

O atributo @ property permite que você use a função vazia da mesma maneira que uma variável de membro (chamando uma função sem parênteses). O atributo const no final significa que não estamos modificando os dados da instância para os quais chamamos vazios . O teste de unidade interno imprime números de 0 a 10.

Outro pequeno recurso é a falta de um construtor explícito. A estrutura Iota possui duas variáveis ​​de membro int. Na instrução foreach do teste, criamos uma instância Iota como se ela tivesse um construtor que aceite duas entradas. Este é um literal estrutural. Quando o compilador D vê isso, mas a estrutura não possui um construtor correspondente, de acordo com a ordem de declaração das variáveis ​​- os membros da estrutura receberão int.

O relacionamento entre os três membros é simples. Se vazio for falso, front é garantido para retornar um novo elemento após a iteração após chamar popFront. Depois de chamar popFront, o valor vazio pode mudar. Se isso for verdade, significa que não há mais elementos para iterar e outras chamadas para frente são inválidas. De acordo com a documentação do InputRange :

  • front pode ser calculado corretamente se e somente se retornar vazio ou retornar falso.
  • front pode ser calculado várias vezes sem chamar popFront ou alterar o intervalo ou os dados subjacentes, e fornece o mesmo resultado para cada cálculo.

Usar expressões foreach, ou loops, não é muito funcional. Suponha que desejemos filtrar todos os números ímpares para Iota. Poderíamos colocar um if dentro de um bloco foreach, mas isso só pioraria. Seria melhor se tivéssemos um intervalo que aceite um intervalo e um predicado que decida se um elemento é adequado ou não.

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

O filtro é novamente muito simples: são necessários Iota e um ponteiro de função. Ao construir o filtro, chamamos testAndIterate, que leva elementos de Iota até que estejam vazios ou que o predicado retorne falso. A idéia é que o predicado decida o que filtrar e o que deixar. As propriedades front e empty são simplesmente traduzidas para Iota. A única coisa que realmente faz o trabalho é popFront. Retorna o item atual e chama testAndIterate. Isso é tudo. Esta é uma implementação de filtro.

Obviamente, testAndIterate tem um loop while, mas reescrevê-lo usando recursão, na minha opinião, é simplesmente estúpido. O que torna D ótimo é que você pode usar o método apropriado para cada tarefa. A programação funcional é boa e muitas vezes ostenta, mas às vezes não é. Se um pequeno montador embutido for necessário ou mais agradável, use.

O filtro de chamadas ainda não parece muito bom. Supondo que você esteja acostumado a ler da esquerda para a direita, o Filtro aparece antes do Iota, mesmo que seja executado depois do Iota. D não fornece uma instrução de pipe, mas usa a sintaxe da chamada de função unificada (UFCS). Se uma expressão puder ser convertida implicitamente no primeiro parâmetro de uma função, a função poderá ser chamada como se fosse uma função membro dessa expressão. Isso é bastante complicado, eu entendo. Um exemplo ajudará:

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

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

O exemplo acima mostra duas chamadas para a função foo. Como afirma implica, ambas as chamadas são equivalentes. O que isso significa para o nosso exemplo de filtro Iota? O UFCS nos permite reescrever o teste de unidade assim:

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

A implementação de mapa / transformação para o intervalo agora deve estar acessível a todos os leitores. Obviamente, um filtro pode ser mais abstrato usando modelos, mas esses são apenas detalhes, nada conceitualmente novo.

Obviamente, existem diferentes tipos de intervalo, por exemplo, bidirecional. Adivinhe quais oportunidades isso oferece a você. Uma dica rápida: existem duas novas primitivas no intervalo bidirecional chamadas back e popBack. Existem outros tipos de intervalo, mas depois de entender o intervalo de entrada mostrado acima duas vezes, você quase todos os reconhece.

PS Apenas para maior clareza, não implemente seu próprio filtro, mapa ou dobra; a biblioteca padrão do Phobos D tem tudo o que você precisa. Dê uma olhada em std.algorithm e std.range .

Sobre o autor


Robert Schadeck obteve um mestrado em ciência da computação pela Universidade de Oldenburg. Sua tese de mestrado foi intitulada “DMCD - D Compithread Multithreaded distribuído com cache”, onde trabalhou na criação de um compilador D a partir do zero. Ele era um estudante de graduação em ciência da computação em 2012-2018. na Universidade de Oldenburg. Sua tese de doutorado se concentra em sistemas de quórum em conjunto com gráficos. Desde 2018, ele tem o prazer de usar D em seu trabalho diário, trabalhando para a Symmetry Investments.

Sobre os investimentos em simetria


A Symmetry Investments é uma empresa de investimento global com escritórios em Hong Kong, Cingapura e Londres. Trabalhamos desde 2014, depois de nos separarmos com sucesso de um grande fundo de hedge de Nova York.

Na Symmetry, nos esforçamos para correr riscos razoavelmente para criar valor para nossos clientes, parceiros e funcionários. Nós nos beneficiamos de nossa capacidade de gerar contratos ganha-ganha - no sentido mais amplo da palavra. Win-Win é o nosso princípio ético e estratégico fundamental. Ao gerar opções ganha-ganha, podemos criar soluções exclusivas que combinam perspectivas que geralmente são consideradas incompatíveis ou opostas e cobrem o melhor que cada uma das partes pode oferecer. Estamos reintegrando a arbitragem de renda fixa com macro estratégias globais. Inventamos e desenvolvemos uma tecnologia que se concentra no potencial da integração homem-máquina. Criamos sistemas nos quais as máquinas fazem o que fazem de melhor, apoiando as pessoas empara que eles façam o que fazem melhor. Criamos uma meritocracia baseada na cooperação: uma cultura na qual a contribuição individual serve tanto a objetivos pessoais quanto coletivos - e é recompensada de acordo. Valorizamos o senso de propriedade e o espírito de equipe de cooperação, auto-realização e comunidade.

O pessoal da Symmetry Investments é membro ativo da Comunidade D desde 2014. Patrocinamos o desenvolvimento de projetos excel-d, dpp, autowrap, libmir e outros. Lançamos o Symmetry Autumn of Code em 2018 e hospedamos o DConf 2019 em Londres.

Aproximadamente. Como o tradutor, como sempre, estava bêbado, não posso garantir a precisão de alguns termos humanos e não entendo nada na última seção. Portanto, envie correções nas comunicações pessoais.

All Articles