Pular para o conteúdo principal

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:

http://www.youtube.com/watch?v=bKzonnwoR2I

"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:

https://gist.github.com/fabiogaluppo/3a5b24cde3afc1dfed19

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:

https://gist.github.com/fabiogaluppo/14702d606b15b503bdd1

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:

https://gist.github.com/fabiogaluppo/eea37226190dfe8ee355

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

2. Decomposição para lowercase:

https://gist.github.com/fabiogaluppo/b3b1d8ad0220ca254548

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

3. Decomposição para sort:

https://gist.github.com/fabiogaluppo/60990d4860e22906946e

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

4. Decomposição para unique:

https://gist.github.com/fabiogaluppo/e5e9355b40fd0e6e561f

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

5. Decomposição para mismatch:

https://gist.github.com/fabiogaluppo/7bbe2918b47fc83f39bf

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

Comentários

Postar um comentário

Postagens mais visitadas deste blog

Mestre Iota

Iota é a nona letra do alfabeto grego, ela é equivalente à letra i do nosso alfabeto. Por convenção ou hábito, utilizamos a letra i na programação para indicar algum tipo de incrementador, como por exemplo, em um for-loop . https://gist.github.com/fabiogaluppo/a23894ae743f7dd29274 Curiosamente, iota , como identificador, também é utilizado na programação para indicar uma sequência finita e consecutiva de números inteiros, como por exemplo, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]. Inclusive, originalmente na STL existia a função iota , inspirada pela linguagem de programação APL , você pode conferir neste link: http://www.sgi.com/tech/stl/iota.html

Valores Aleatórios Simplificados

A partir do C++ 11, foi introduzido o header <random>  com diversos facilitadores para suporte de geração de números aleatórios. A produção destes números é feita através da combinação de duas categorias de objetos: os geradores e os distribuidores. Os geradores, são responsáveis pela geração dos números, e os distribuidores são responsáveis pela transformação dos números gerados em algum tipo de distribuição de probabilidade.  Como por exemplo, uma distribuição normal (aquela da Gaussiana) ou uma distribuição de Pareto (aquela do 80-20). As opções não faltam, como você pode ver nas referências, por exemplo:   http://www.cplusplus.com/reference/random/ ou  http://en.cppreference.com/w/cpp/header/random .

Policy-based design: log writer

Policy-based design Vamos neste artigo dar mais uma pincelada no Policy-based design . Vamos fazer como exemplo uma classe de log. Como este é só um exemplo, não vamos considerar múltiplos parâmetros no log, mas somente uma string, assim não fugiremos do assunto. Uma das coisas mais importantes neste tipo de design é o desacoplamento. Ele é uma excelente alternativa ao uso de interfaces por duas razões: Não gera chamadas virtuais (ou um nível de indireção em tempo de execução) Duck typing ( https://pt.wikipedia.org/wiki/Duck_typing ) Eu gosto bastante desse tipo de design, já usado aqui: http://simplycpp.com/2016/02/05/leitura-de-configuracao-em-c/