Oto moje argumenty przemawiające za tym, dlaczego programowanie funkcjonalne może i powinno być wykorzystywane w nauce obliczeniowej. Korzyści są ogromne, a wady szybko znikają. Moim zdaniem jest tylko jeden oszust:
Wada : brak obsługi języka w C / C ++ / Fortran
Przynajmniej w C ++ ta zniknie - ponieważ C ++ 14/17 dodał potężne narzędzia do obsługi programowania funkcjonalnego. Być może będziesz musiał napisać kod biblioteki / wsparcia samodzielnie, ale językiem będzie twój przyjaciel. Jako przykład podajemy bibliotekę (ostrzeżenie: wtyczka), która wykonuje niezmienne wielowymiarowe tablice w C ++: https://github.com/jzrake/ndarray-v2 .
Oto link do dobrej książki na temat programowania funkcjonalnego w C ++, chociaż nie jest on skoncentrowany na aplikacjach naukowych.
Oto moje podsumowanie tego, co według mnie jest profesjonalistą:
Plusy :
- Poprawność
- Wyrozumiałość
- Występ
Jeśli chodzi o poprawność , programy funkcjonalne są wyraźnie dobrze postawione : zmuszają cię do prawidłowego zdefiniowania minimalnego stanu zmiennych fizycznych oraz funkcji, która przesuwa ten stan do przodu w czasie:
int main()
{
auto state = initial_condition();
while (should_continue(state))
{
state = advance(state);
side_effects(state);
}
return 0;
}
Rozwiązanie równania różniczkowego cząstkowego (lub ODE) jest idealne do programowania funkcjonalnego; po prostu zastosujesz czystą funkcję ( advance
) do bieżącego rozwiązania, aby wygenerować następne.
Z mojego doświadczenia wynika, że oprogramowanie do symulacji fizyki jest zasadniczo obciążone złym zarządzaniem stanem . Zwykle każdy etap algorytmu działa na pewnym stanie wspólnego (efektywnie globalnego) stanu. Utrudnia to, a nawet uniemożliwia, zapewnienie prawidłowej kolejności operacji, pozostawiając oprogramowanie podatne na błędy, które mogą objawiać się jako błędy seg, lub, co gorsza, warunki błędów, które nie powodują awarii kodu, ale dyskretnie naruszają integralność jego wiedzy wynik. Próba zarządzania stanem współdzielonym w symulacji fizyki również hamuje wielowątkowość - co stanowi problem na przyszłość, ponieważ superkomputery zmierzają w kierunku większej liczby rdzeni, a skalowanie z MPI często kończy się na ~ 100 tys. Zadań. Natomiast programowanie funkcjonalne sprawia, że paralelizm pamięci współużytkowanej jest trywialny ze względu na niezmienność.
Wydajność poprawia się również w programowaniu funkcjonalnym ze względu na leniwą ocenę algorytmów (w C ++ oznacza to generowanie wielu typów w czasie kompilacji - często po jednym dla każdej aplikacji funkcji). Ale zmniejsza obciążenie dostępu do pamięci i przydziałów, a także eliminuje wirtualną wysyłkę - pozwalając kompilatorowi zoptymalizować cały algorytm, widząc jednocześnie wszystkie obiekty funkcyjne, które go zawierają. W praktyce będziesz eksperymentować z różnymi ustawieniami punktów oceny (w których wynik algorytmu jest buforowany do bufora pamięci), aby zoptymalizować wykorzystanie procesora w porównaniu do alokacji pamięci. Jest to raczej łatwe ze względu na dużą lokalizację (patrz przykład poniżej) etapów algorytmu w porównaniu z tym, co zwykle zobaczysz w module lub kodzie opartym na klasach.
Programy funkcjonalne są łatwiejsze do zrozumienia, o ile trywializują stan fizyki. Nie oznacza to, że ich składnia jest zrozumiała dla wszystkich twoich kolegów! Autorzy powinni zachować ostrożność przy korzystaniu z dobrze nazwanych funkcji, a badacze powinni przyzwyczaić się do tego, że algorytmy są wyrażane funkcjonalnie, a nie proceduralnie. Przyznaję, że brak struktur kontrolnych może być dla niektórych zniechęcający, ale nie sądzę, że powinno to powstrzymywać nas przed pójściem w przyszłość, by móc robić lepszą naukę na komputerach.
Poniżej znajduje się przykładowa advance
funkcja, zaadaptowana z kodu o skończonej objętości za pomocą ndarray-v2
pakietu. Zwróć uwagę na to_shared
operatory - są to punkty oceny, o których wspominałem wcześniej.
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() };
}