Jakie są zalety i wady związane z używaniem klas do enkapsulacji algorytmów numerycznych?

13

Wiele algorytmów wykorzystywanych w obliczeniach naukowych ma inną wewnętrzną strukturę niż algorytmy powszechnie uważane za mniej wymagające matematyki formy inżynierii oprogramowania. W szczególności poszczególne algorytmy matematyczne są zwykle bardzo złożone, często obejmują setki lub tysiące wierszy kodu, ale mimo to nie obejmują żadnego stanu (tj. Nie działają na złożoną strukturę danych) i często można je sprowadzić - pod względem programowym interfejs - do pojedynczej funkcji działającej na tablicę (lub dwie).

Sugeruje to, że funkcja, a nie klasa, jest naturalnym interfejsem dla większości algorytmów spotykanych w obliczeniach naukowych. Jednak ten argument nie daje wglądu w sposób, w jaki należy obsługiwać implementację złożonych, wieloczęściowych algorytmów.

Podczas gdy tradycyjnym podejściem było po prostu posiadanie jednej funkcji, która wywołuje wiele innych funkcji, przekazując odpowiednie argumenty po drodze, OOP oferuje inne podejście, w którym algorytmy mogą być enkapsulowane jako klasy. Dla jasności, enkapsulując algorytm w klasie, mam na myśli stworzenie klasy, w której dane wejściowe algorytmu są wprowadzane do konstruktora klasy, a następnie wywoływana jest metoda publiczna w celu faktycznego wywołania algorytmu. Taka implementacja multigrid w psuedocode C ++ może wyglądać następująco:

class multigrid {
    private:
        x_, b_
        [grid structure]

        restrict(...)
        interpolate(...)
        relax(...)
    public:
        multigrid(x,b) : x_(x), b_(b) { }
        run()
}

multigrid::run() {
     [call restrict, interpolate, relax, etc.]
}

Moje pytanie brzmi zatem: jakie są zalety i wady tego rodzaju praktyki w porównaniu z bardziej tradycyjnym podejściem bez zajęć? Czy istnieją problemy związane z rozszerzalnością lub konserwacją? Żeby było jasne, nie zamierzam zabiegać o opinię, ale raczej lepiej rozumieć dalsze skutki (tj. Te, które mogą się nie pojawić, dopóki baza kodów nie stanie się dość duża) przyjęcia takiej praktyki kodowania.

Ben
źródło
2
To zawsze zły znak, gdy nazwa twojej klasy jest przymiotnikiem, a nie rzeczownikiem.
David Ketcheson
3
Klasa może służyć jako bezstanowa przestrzeń nazw do organizowania funkcji w celu zarządzania złożonością, ale istnieją też inne sposoby zarządzania złożonością w językach udostępniających klasy. (Przestrzenie nazw w C ++ i moduły w Pythonie przyjść do głowy.)
Geoff Oxberry
@GeoffOxberry Nie mogę porozmawiać o tym, czy jest to dobre czy złe użycie - dlatego przede wszystkim pytam - ale klasy, w przeciwieństwie do przestrzeni nazw lub modułów, mogą również zarządzać „stanem tymczasowym”, np. Hierarchią siatki w trybie wielosieciowym, który jest odrzucany po zakończeniu algorytmu.
Ben

Odpowiedzi:

13

Po 15 latach programowania numerycznego mogę jednoznacznie stwierdzić, co następuje:

  • Kapsułkowanie jest ważne. Nie chcesz przekazywać wskaźników do danych (jak sugerujesz), ponieważ ujawnia to schemat przechowywania danych. Jeśli ujawnisz schemat przechowywania, nigdy nie możesz go zmienić ponownie, ponieważ będziesz mieć dostęp do danych w całym programie. Jedynym sposobem, aby tego uniknąć, jest kapsułkowanie danych w prywatnych zmiennych członka klasy i pozwolenie, aby działały na nim tylko funkcje członka. Jeśli przeczytam twoje pytanie, pomyśl o funkcji, która oblicza wartości własne macierzy jako bezstanowe, przyjmując wskaźnik do pozycji macierzy jako argument i zwracając wartości własne w jakiś sposób. Myślę, że to zły sposób myślenia o tym. Moim zdaniem ta funkcja powinna być „stałą” funkcją składową klasy - nie dlatego, że zmienia macierz, ale dlatego, że jest to ta, która działa z danymi.

  • Większość języków programowania OO umożliwia korzystanie z prywatnych funkcji członka. To jest twój sposób na rozbicie jednego dużego algorytmu na mniejszy. Na przykład różne funkcje pomocnicze potrzebne do obliczania wartości własnej nadal działają na macierzy, a więc naturalnie byłyby to prywatne funkcje składowe klasy macierzy.

  • W porównaniu z wieloma innymi systemami oprogramowania może być prawdą, że hierarchie klas są często mniej ważne niż, powiedzmy, w graficznych interfejsach użytkownika. Z pewnością są miejsca w oprogramowaniu numerycznym, w których są one widoczne - Jed przedstawia jedną na drugą odpowiedź na ten wątek, a mianowicie na wiele sposobów, w jakie można przedstawić matrycę (lub, bardziej ogólnie, operator liniowy w skończonej przestrzeni wektorowej). PETSc robi to bardzo konsekwentnie, z funkcjami wirtualnymi dla wszystkich operacji, które działają na macierzach (nie nazywają tego „funkcjami wirtualnymi”, ale tak właśnie jest). Istnieją inne obszary w typowych kodach elementów skończonych, w których stosuje się tę zasadę projektowania oprogramowania OO. Przychodzą mi na myśl różne formuły kwadraturowe i wiele elementów skończonych, z których wszystkie są naturalnie reprezentowane jako jeden interfejs / wiele implementacji. Do tej grupy należałyby także opisy prawa materialnego. Ale może być prawdą, że o to chodzi i że reszta kodu elementu skończonego nie używa dziedziczenia tak wszechobecnie, jak można go użyć, powiedzmy, w GUI.

Tylko z tych trzech punktów powinno być jasne, że programowanie obiektowe zdecydowanie ma również zastosowanie do kodów numerycznych i że głupotą byłoby ignorowanie wielu zalet tego stylu. Może prawdą jest, że BLAS / LAPACK nie używa tego paradygmatu (i że zwykły interfejs ujawniany przez MATLAB też nie), ale zaryzykuję przypuszczenie, że każde udane oprogramowanie numeryczne napisane w ciągu ostatnich 10 lat jest w rzeczywistości obiektowy.

Wolfgang Bangerth
źródło
16

Hermetyzacja i ukrywanie danych są niezwykle ważne dla bibliotek rozszerzalnych w obliczeniach naukowych. Rozważ macierze i solwery liniowe jako dwa przykłady. Użytkownik musi po prostu wiedzieć, że operator jest liniowy, ale może mieć wewnętrzną strukturę, taką jak rzadkość, jądro, hierarchiczna reprezentacja, produkt tensorowy lub uzupełnienie Schura. We wszystkich przypadkach metody Kryłowa nie zależą od szczegółów operatora, zależą jedynie od działania MatMultfunkcji (i być może jej przylegania). Podobnie użytkownik interfejsu liniowego solvera (np. Solvera nieliniowego) dba tylko o to, by problem liniowy został rozwiązany i nie powinien lub nie chce określać używanego algorytmu. Rzeczywiście, określenie takich rzeczy utrudniłoby zdolność nieliniowego solvera (lub innego interfejsu zewnętrznego).

Interfejsy są dobre. W zależności od implementacji jest źle. To, czy osiągniesz to za pomocą klas C ++, obiektów C, klas Haskell, czy jakiejś innej funkcji językowej, nie ma znaczenia. Możliwości, niezawodność i rozszerzalność interfejsu mają znaczenie w bibliotekach naukowych.

Jed Brown
źródło
8

Klasy powinny być używane tylko wtedy, gdy struktura kodu jest hierarchiczna. Ponieważ wspominasz o algorytmach, ich naturalną strukturą jest schemat blokowy, a nie hierarchia obiektów.

W przypadku OpenFOAM część algorytmiczna jest implementowana w kategoriach operatorów ogólnych (div, grad, curl itp.), Które są zasadniczo abstrakcyjnymi funkcjami działającymi na różnych typach tensorów, wykorzystujących różne typy schematów numerycznych. Ta część kodu jest zasadniczo zbudowana z wielu ogólnych algorytmów działających na klasach. Pozwala to klientowi napisać coś takiego:

solve(ddt(U) + div(phi, U)  == rho*g + ...);

Hierarchie, takie jak modele transportu, modele turbulencji, schematy różnicowania, schematy gradientu, warunki brzegowe itp. Są zaimplementowane w kategoriach klas C ++ (ponownie, ogólne na wielkościach tensorów).

Zauważyłem podobną strukturę w bibliotece CGAL, w której różne algorytmy są spakowane razem jako grupy obiektów funkcyjnych w pakiecie z informacjami geometrycznymi w celu utworzenia geometrycznych jąder (klas), ale to znów odbywa się w celu oddzielenia operacji od geometrii (usunięcie punktu z geometrii twarz, od typu danych punktowych).

Struktura hierarchiczna ==> klasy

Algorytmy proceduralne, schemat ==>

tmaric
źródło
5

Nawet jeśli jest to stare pytanie, myślę, że warto wspomnieć o szczególnym rozwiązaniu Julii . Tym językiem jest „OOP bezklasowy”: głównymi konstrukcjami są typy, tj. Złożone obiekty danych podobne do structs w C, na których zdefiniowano relację dziedziczenia. Typy nie mają „funkcji składowych”, ale każda funkcja ma podpis typu i akceptuje podtypy. Na przykład można mieć abstrakcyjny Matrixtyp i podtypy DenseMatrix, SparseMatrixi mają ogólny sposób do_something(a::Matrix, b::Matrix)ze specjalizacji do_something(a::SparseMatrix, b::SparseMatrix). Wysyłanie wielokrotne służy do wybierania najbardziej odpowiedniej wersji do wywołania.

To podejście jest silniejsze niż OOP oparty na klasach, co jest równoważne z wysyłaniem opartym na dziedziczeniu tylko na pierwszym argumencie, jeśli przyjmiesz konwencję, że „metoda jest funkcją thisjako pierwszym parametrem” (powszechne np. W Pythonie). Niektóre formy wielokrotnej wysyłki mogą być emulowane w, powiedzmy, C ++, ale przy znacznych zniekształceniach .

Główne rozróżnienie polega na tym, że metody nie należą do klas, ale istnieją jako osobne byty i dziedziczenie może nastąpić dla wszystkich parametrów.

Niektóre referencje:

http://docs.julialang.org/en/release-0.4/manual/methods/

http://assoc.tumblr.com/post/71454527084/cool-things-you-can-do-in-julia

https://thenewphalls.wordpress.com/2014/03/06/understanding-object-oriented-programming-in-julia-inheritance-part-2/

Federico Poloni
źródło
1

Dwie zalety podejścia OO to:

  • βαcalculate_alpha()αcalculate_beta()calculate_alpha()α

  • calculate_f()f(x,y,z)zset_z()zcalculate_f()z

ptomato
źródło