Aqui estão meus argumentos sobre por que a programação funcional pode e deve ser utilizada para a ciência da computação. Os benefícios são vastos e os contras estão desaparecendo rapidamente. Na minha opinião, há apenas um golpe:
Con : falta de suporte ao idioma em C / C ++ / Fortran
Pelo menos em C ++, esse golpe está desaparecendo - pois o C ++ 14/17 adicionou recursos poderosos para oferecer suporte à programação funcional. Você pode precisar escrever algum código de biblioteca / suporte, mas o idioma será seu amigo. Como exemplo, aqui está uma biblioteca (warning: plug) que faz matrizes multidimensionais imutáveis em C ++: https://github.com/jzrake/ndarray-v2 .
Além disso, aqui está um link para um bom livro sobre programação funcional em C ++, embora não esteja focado em aplicativos científicos.
Aqui está o meu resumo do que acredito ser o profissional:
Prós :
- Correção
- Compreensibilidade
- atuação
Em termos de correção , os programas funcionais são manifestamente bem colocados : forçam você a definir adequadamente o estado mínimo de suas variáveis físicas e a função que avança esse estado no tempo:
int main()
{
auto state = initial_condition();
while (should_continue(state))
{
state = advance(state);
side_effects(state);
}
return 0;
}
Resolver uma equação diferencial parcial (ou ODE) é perfeita para programação funcional; você está apenas aplicando uma função pura ( advance
) à solução atual para gerar a próxima.
Na minha experiência, o software de simulação física é, em geral, sobrecarregado pelo mau gerenciamento do estado . Normalmente, cada estágio do algoritmo opera em alguma parte de um estado compartilhado (efetivamente global). Isso torna difícil, ou mesmo impossível, garantir a ordem correta das operações, deixando o software vulnerável a erros que podem se manifestar como seg-falhas, ou pior, termos de erro que não causam erro no código, mas comprometem silenciosamente a integridade de sua ciência. saída. Tentar gerenciar o estado compartilhado em uma simulação física também inibe a multiencadeamento - o que é um problema para o futuro, pois os supercomputadores estão se movendo em direção a contagens mais altas de núcleo, e o dimensionamento com o MPI geralmente chega a ~ 100k tarefas. Por outro lado, a programação funcional torna trivial o paralelismo de memória compartilhada, devido à imutabilidade.
O desempenho também é aprimorado na programação funcional devido à avaliação lenta dos algoritmos (em C ++, isso significa gerar muitos tipos em tempo de compilação - geralmente um para cada aplicativo de uma função). Mas reduz a sobrecarga de acessos e alocações de memória, além de eliminar o despacho virtual - permitindo que o compilador otimize um algoritmo inteiro vendo de uma vez todos os objetos de função que o compõem. Na prática, você experimentará diferentes arranjos dos pontos de avaliação (onde o resultado do algoritmo é armazenado em cache em um buffer de memória) para otimizar o uso da CPU versus alocações de memória. Isso é bastante fácil devido à alta localidade (veja o exemplo abaixo) dos estágios do algoritmo em comparação com o que você normalmente vê em um módulo ou código baseado em classe.
Os programas funcionais são mais fáceis de entender na medida em que trivializam o estado da física. Isso não quer dizer que sua sintaxe seja facilmente compreendida por todos os seus colegas! Os autores devem ter cuidado ao usar funções bem nomeadas, e os pesquisadores em geral devem se acostumar a ver os algoritmos expressos funcionalmente e não processualmente. Admito que a ausência de estruturas de controle pode ser desagradável para alguns, mas não acho que isso deva nos impedir de ir para o futuro, capazes de fazer ciência de melhor qualidade em computadores.
Abaixo está uma advance
função de amostra , adaptada de um código de volume finito usando o ndarray-v2
pacote. Observe os to_shared
operadores - esses são os pontos de avaliação que eu aludi anteriormente.
auto advance(const solution_state_t& state)
{
auto dt = determine_time_step_size(state);
auto du = state.u
| divide(state.vertices | volume_from_vertices)
| nd::map(recover_primitive)
| extrapolate_boundary_on_axis(0)
| nd::to_shared()
| compute_intercell_flux(0)
| nd::to_shared()
| nd::difference_on_axis(0)
| nd::multiply(-dt * mara::make_area(1.0));
return solution_state_t {
state.time + dt,
state.iteration + 1,
state.vertices,
state.u + du | nd::to_shared() };
}