beginner_intermediate

Composição, parte 1 – com exemplo clássico do Unix

A idéia deste post é reproduzir a demonstração clássica feita por Brian Kernighan utilizando composição de pequenos programas através dos Unix pipelines. Ela é bem simples, porém muito significativa, se trata desta sequência:

makewords text_file | lowercase | sort | unique | mismatch dictionary_file

Onde o objetivo é identificar quais palavras do texto (text_file) são desconhecidas pelo dicionário (dictionary_file) informado:

AT&T Archives – The UNIX System: Making Computers More Productive

A demonstração ocorre no vídeo do link acima no trecho de 5:35 a 10:50 – o vídeo por completo é bem interessante e vale a pena ser assistido.  Brian Kernighan também aparece falando sobre o assunto (excluindo a demo) neste vídeo:

So, pipeline is basically a mechanism for connecting the output of one program directly/conveniently into the input of another program” — Brian Kernighan

Então vamos demonstrar isto de duas maneiras com C++ moderno, da forma original (com pequenos programas atuando em conjunto) e primeiramente através de um programa monolítico, onde a composição ocorre por meio das suas funções. Obviamente que mantendo o comportamento uniforme entre as implementações.

A invenção do pipeline foi do matemático, engenheiro e programador Doug McIlroy.

DougMcIlroy

Note que grifei a palavra matemático ao citá-lo. Pois a composição de funções vem da Matemática e certamente foi o elemento inspirador para a construção dos famosos Unix pipelines e, curiosamente, os pipelines são antecessores ao próprio Unix. A idéia da composição não é exclusiva a um ambiente, pode ser aplicado em diversas situações, inclusive no que é a sensação do momento: Microserviços.

Voltando ao exemplo – no caso, o monolítico onde a composição ocorre através das suas funções:

Acima, você notará que a saída de uma função (makewords) é a entrada da outra (lowercase). Perceba que estamos usando rvalues (&&) no parâmetro de entrada das funções e move constructor (std::move + container suportando move semantics) no retorno ou saída das funções. Estes dois recursos fazem parte do C++ moderno, para dar usabilidade e evitar perda de desempenho ao copiar um container como std::vector com uma grande quantidade de elementos.

Por estarmos trabalhando com rvalues, ou seja, valores temporários de uso contextualizado, a composição destas funções ocorrerá da seguinte maneira:

Ao compilar e rodar o programa com os arquivos de teste e dicionário, o resultado será:

https://github.com/SimplyCpp/examples/blob/master/mismatch/mismatch_program.cpp

A segunda parte consiste em quebrar ou decompor este monolíto (mismatch_program.cpp) em diversos pequenos programas e reestabeler a forma original da composição da demonstração citada no início deste post. Onde a sequência:

_makewords .\tests\test.txt | _lowercase | _sort | _unique | _mismatch .\tests\words.txt

Resultará em:

https://github.com/SimplyCpp/examples/tree/master/mismatch

Obviamente que o mesmo resultado com o exemplo monolítico. Exceto se houver algum erro, como o erro descoberto por acaso durante a elaboração deste post, onde o autor perdeu algumas horas depurando para chegar na conclusão deste bug. 🙂

A idéia do pipeline (do sistema operacional)  é estabelecer o encadeamento de um  conjunto de processos que são conectados através das streams padrões (stdin, stdout, stderr), portanto a saída de cada processo (stdout) é a entrada (stdin) do próximo processo, e assim sucessivamente. O que trafega nestes encanamentos são sequências de bytes, normalmente interpretadas como texto (string). No entanto, mesmo para sistemas operacionais, outros modelos podem ser encontrados/implementados, como por exemplo a serialização e deserialização de objetos compartilhados entre processos utilizando alguma forma de IPC.

Para a decomposição do monolíto, implementamos cada uma das funções num programa independente. Cada um deles adaptado para suportar streams, que no C++ é representado pelo conjunto de bibliotecas iostream. No C++, stdin é std::cinstdout é std::cout e stderr é std::cerr – herdam de istream (std::cin) ou de ostream (std::cout e std::cerr). O std::ifstream também é utilizado se a leitura for feita através de um arquivo, ele é uma stream para leitura de arquivos e herda de istream.

1. Decomposição para makewords:

Programa completo: https://github.com/SimplyCpp/examples/blob/master/mismatch/_makewords.cpp

2. Decomposição para lowercase:

Programa completo: https://github.com/SimplyCpp/examples/blob/master/mismatch/_lowercase.cpp

3. Decomposição para sort:

Programa completo: https://github.com/SimplyCpp/examples/blob/master/mismatch/_sort.cpp

4. Decomposição para unique:

Programa completo: https://github.com/SimplyCpp/examples/blob/master/mismatch/_unique.cpp

5. Decomposição para mismatch:

Programa completo: https://github.com/SimplyCpp/examples/blob/master/mismatch/_mismatch.cpp

Uma observação curiosa, o ato da decomposição permite a composição, algo do tipo “decompor para compor”. Portanto, quando temos um monolíto, ao quebrarmos ou decompormos, poderemos utilizar partes ou todas as peças de uma maneira composicional, juntando peças como os famosos Legos. O que estou querendo dizer é: a idéia da arquitetura monolítica e da arquitetura de microserviços são muito mais do que uma novidade de sistemas distribuídos em alta escala, e permeia a boa programação a décadas.

Fontes:
https://github.com/SimplyCpp/examples/tree/master/mismatch

1 Comment

Leave a Reply

Your email address will not be published. Required fields are marked *