Pular para o conteúdo principal

Por quem os ponteiros dobram, estrelando std::accumulate

O std::accumulate é um algoritmo de operação numérica, da mesma forma que std::iota explorado anteriormente (http://simplycpp.com/2015/11/06/mestre-iota/), reside no header <numeric> da STL: http://www.cplusplus.com/reference/numeric/accumulate/.

Seu objetivo, até mesmo porque o nome desta função dá uma dica, é acumular elementos que pertencem a um range fornecido por um par de iterators (usualmente begin e end para uma sequência completa). O std::accumulate também possui um valor inicial para o acumulador que é fornecido como terceiro parâmetro desta função.

Supondo um array de tamanho 10 e já inicializado como container de referência para os exemplos a seguir:



https://gist.github.com/fabiogaluppo/3091dbf3ecf3b4061be0

A utilização padrão do std::accumulate com um container qualquer (assim como o indicado acima) é da seguinte forma:

https://gist.github.com/fabiogaluppo/8e8c4c0cbd3640c5eaa4

Onde temos como retorno a soma de todos os elementos do container. Com um container como o array em mãos, logo é possível saber seu tamanho através do membro do tipo função size. Assim podemos, por exemplo, extrair a média de um jeito muito simples:

https://gist.github.com/fabiogaluppo/2bd1badab7fb81b37cbc

Perceba que o valor inicial na chamada anterior retornará um tipo double, em função do valor inicial fornecido ser deste tipo. Então é fácil concluir, tanto pela assinatura de std::accumulate quanto pelo retorno do exemplo anterior, que o tipo inicial e o tipo de retorno serão os mesmos e que o valor do iterator quando for derefenciado deverá ser compatível ou convertível para este tipo.

Nos posts passados, mostramos como é possível ter iterator com predicado e alterar o comportamento do operator++ do tipo fornecido para o std::iota. Podemos juntar estas duas técnicas e fazer o mesmo para o tipo inicial fornecido como terceiro parâmetro da função, como no caso a seguir demonstrado em accumulator_with_predicate. Aqui vai o consumo (note que está sendo usado um intervalo/range parcial do container, e este será filtrado com orientação do predicado indicado):

https://gist.github.com/fabiogaluppo/74e72907ac42b9c5fca1

E o tipo que oferece um novo significado para o operator+ e é convertível para o tipo T compatível com a variável que recebe o retorno da função:

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

Caso ache o algoritmo numérico std::accumulate verboso para apenas somar elementos (de uma parte) do container, sugiro a criação de uma função especialista, como o par de funções sum implementadas em termos de std::accumulate:

https://gist.github.com/fabiogaluppo/9ba7c3fe101ab0beeb0b

Onde o consumo se torna simplificado e extremamente declarativo:

https://gist.github.com/fabiogaluppo/0d9dafde1c5acb6867c3

Outro conceito relacionado com std::accumulate é o fold do estilo de programação funcional com higher-order functions. O std::accumulate (assim como outras funções da biblioteca padrão do C++) é uma higher-order function, que tem um functor ou objeto do tipo função (ou um lambda) como quarto parâmetro a ser informado. Neste caso, é possível mudar o comportamento padrão que é a soma (fornecido pela função std::plus) por outra função com operação binária, como por exemplo uma multiplicação. Suponha que seu objetivo é criar uma função fold no estilo STL:

https://gist.github.com/fabiogaluppo/21970867753efa1c2346

Acima temos a implementação de um fold left, onde a expansão da aplicação consecutiva da função f ocorre pela esquerda, algo interpretado na forma: ...f(f(f(f(f(acc, 1),2),3),4),5)..., se usarmos um reverse iterator teremos um fold right que pode ser interpretado na forma: ...f(f(f(f(f(acc, 5),4),3),2),1)..., as duas figuras a seguir, retiradas da página do Fold na Haskell.org, dá uma noção visual do fold left (foldl) e fold right (foldr):

foldl

foldr

No entanto, não precisamos desta função fold, pois o próprio std::accumulate é o fold! E tudo que é feito com fold é possível reproduzir com std::accumulate e vice-versa.

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

Dentre as coisas interessantes com std::accumulate (ou fold), conseguimos calcular um hash simples para uma string da seguinte maneira:

https://gist.github.com/fabiogaluppo/4dca638ca6c92683bc2a

Vale lembrar que executar um fold left ou um fold right (std::accumulate com o par de iteradores begin/end e std::accumulate com o par de iteradores rbegin/rend, respectivamente) não garantirá uma equabilidade, pois isto dependerá da operação binária e seu tipo possuir a propriedade comutativa. Por padrão a adição de tipos integrais (short, int, long) e tipos reais (float e double) possuem a propriedade comutativa da adição e da multiplicação:

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

Porém, a concatenação de strings não possui esta propriedade como podemos ver a seguir:

https://gist.github.com/fabiogaluppo/6ca06c10f47f2f07b62f

A operação fold possui como sua dualidade a operação unfold. Note que fold (ou std::accumulate) retorna um valor combinado, uma espécie de monólito. Imagine que você queira a partir deste bloco obter toda a sequência que o originou. Este é um trabalho para unfold, como segue no exemplo com a acumulação da progressão aritmética gerada:

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

https://gist.github.com/fabiogaluppo/8a92d515241d0edb6b9a

Como demonstrado neste post, o std::accumulate, apesar de ter um propósito bem específico – acumular ou fazer agregação, é uma função bem versátil. Ela também é conhecida popularmente com outros nomes além de fold, são eles: reduce ou aggregate.

Adendo sobre comutatividade e tipos com ponto flutuante (float ou double):

https://gist.github.com/fabiogaluppo/6e8a7464fab8c1cdc515

Fonte:
https://github.com/SimplyCpp/examples/blob/master/understanding_accumulate_program.cpp

Comentários

  1. Hi! Post bacana, mas você disse que adição em float é comutativa e isso não procede né?

    ResponderExcluir
  2. Olá Ricardo,

    Obrigado pela observação muito pertinente!

    Matematicamente, os números reais são um grupo abeliano (ou grupo comutativo) sob a adição, e os números reais não incluindo o zero são um grupo abeliano sob a multiplicação. Logo, a comutatividade estará ok.

    Computacionalmente, para tipos reais ou de ponto flutuante (float ou double) as operações poderão sofrer uma taxa de erro, nestes casos a comparação usando um "epsilon" se torna necessária. Uma abordagem completa sobre este assunto pode ser encontrada aqui: https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/

    Fiz um adendo no post com alguns exemplos.

    Grande abraço,

    ResponderExcluir

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/