Po prostu ogólnie niezmienne typy tworzone w językach, które nie obracają się wokół niezmienności, będą kosztować więcej czasu programisty, aby je stworzyć, a także potencjalnie wykorzystać, jeśli wymagają jakiegoś typu „konstruktora” do wyrażenia pożądanych zmian (nie oznacza to, że ogólny praca będzie większa, ale w takich przypadkach koszty są z góry). Niezależnie od tego, czy język naprawdę ułatwia tworzenie niezmiennych typów, czy nie, zawsze będzie wymagał trochę przetwarzania i narzutu pamięci w przypadku nietrywialnych typów danych.
Tworzenie funkcji pozbawionych efektów ubocznych
Jeśli pracujesz w językach, które nie obracają się wokół niezmienności, myślę, że pragmatycznym podejściem nie jest dążenie do tego, aby każdy typ danych był niezmienny. Potencjalnie znacznie bardziej produktywny sposób myślenia, który daje wiele takich samych korzyści, polega na maksymalizacji liczby funkcji w systemie, które powodują zerowe skutki uboczne .
Jako prosty przykład, jeśli masz funkcję, która powoduje taki efekt uboczny:
// Make 'x' the absolute value of itself.
void make_abs(int& x);
Zatem nie potrzebujemy niezmiennego typu danych całkowitoliczbowych, który zabrania operatorom takich jak przypisanie po inicjalizacji, aby funkcja ta unikała skutków ubocznych. Możemy to po prostu zrobić:
// Returns the absolute value of 'x'.
int abs(int x);
Teraz funkcja nie zadziera x
ani nie wykracza poza jej zakres, aw tym trywialnym przypadku moglibyśmy nawet ogolić niektóre cykle, unikając narzutu związanego z pośrednim / aliasingiem. Przynajmniej druga wersja nie powinna być droższa pod względem obliczeniowym niż pierwsza.
Rzeczy, których kopiowanie w całości jest drogie
Oczywiście większość przypadków nie jest tak trywialna, jeśli chcemy uniknąć sytuacji, w której funkcja powoduje skutki uboczne. Złożony przypadek użycia w świecie rzeczywistym może być mniej więcej taki:
// Transforms the vertices of the specified mesh by
// the specified transformation matrix.
void transform(Mesh& mesh, Matrix4f matrix);
W tym momencie siatka może wymagać kilkuset megabajtów pamięci z ponad sto tysiącami wielokątów, jeszcze większą liczbą wierzchołków i krawędzi, wieloma mapami tekstur, zmiennymi celami itp. Kopiowanie całej siatki byłoby bardzo drogie, aby to zrobić transform
funkcja wolna od efektów ubocznych, takich jak:
// Returns a new version of the mesh whose vertices been
// transformed by the specified transformation matrix.
Mesh transform(Mesh mesh, Matrix4f matrix);
I w tych przypadkach, gdy kopiowanie czegoś w całości byłoby zwykle epickim narzutem, przydało mi się zmienić Mesh
w trwałą strukturę danych i niezmienny typ z analogicznym „konstruktorem” do tworzenia zmodyfikowanych wersji, aby to można po prostu płytko skopiować i zliczyć części, które nie są unikalne. Wszystko to skupia się na możliwości pisania funkcji siatki, które są wolne od skutków ubocznych.
Trwałe struktury danych
A w tych przypadkach, w których kopiowanie wszystkiego jest tak niewiarygodnie drogie, starałem się zaprojektować niezmienną, Mesh
aby naprawdę się spłaciła, mimo że z góry miała nieco stromy koszt, ponieważ nie tylko uprościła bezpieczeństwo wątków. Uprościło to również nieniszczącą edycję (pozwalającą użytkownikowi na warstwowanie operacji siatki bez modyfikowania oryginalnej kopii), cofanie systemów (teraz system cofania może po prostu przechowywać niezmienną kopię siatki przed zmianami dokonanymi przez operację bez wysadzania pamięci use) oraz bezpieczeństwo wyjątków (teraz, jeśli w powyższej funkcji wystąpi wyjątek, funkcja nie musi cofać i cofać wszystkich swoich skutków ubocznych, ponieważ nie spowodowała żadnych).
Mogę śmiało powiedzieć w tych przypadkach, że czas potrzebny do uczynienia tych potężnych struktur danych niezmiennymi zaoszczędził więcej czasu niż koszt, ponieważ porównałem koszty utrzymania tych nowych projektów z poprzednimi, które obracały się wokół zmienności i funkcji powodujących skutki uboczne, a poprzednie modyfikowalne projekty kosztowały znacznie więcej czasu i były znacznie bardziej podatne na ludzkie błędy, szczególnie w obszarach, które naprawdę kuszą deweloperów do zaniedbania w czasie kryzysu, takich jak bezpieczeństwo wyjątkowe.
Sądzę więc, że niezmienne typy danych naprawdę się opłacają w takich przypadkach, ale nie wszystko musi być niezmienne, aby większość funkcji w twoim systemie była wolna od skutków ubocznych. Wiele rzeczy jest na tyle tanie, że można je w całości skopiować. Również wiele aplikacji w świecie rzeczywistym będzie musiało wywoływać tu i tam pewne skutki uboczne (przynajmniej jak zapisywanie pliku), ale zazwyczaj jest o wiele więcej funkcji, które mogą być pozbawione skutków ubocznych.
Chodzi o to, aby mieć dla mnie pewne niezmienne typy danych, aby upewnić się, że możemy napisać maksymalną liczbę funkcji, które będą wolne od skutków ubocznych, bez ponoszenia epickich kosztów ogólnych w postaci głębokiego kopiowania ogromnych struktur danych w lewo i prawo w całości, gdy tylko małe porcje z nich należy zmodyfikować. Mając w tych przypadkach trwałe struktury danych, stają się one szczegółami optymalizacji, dzięki czemu możemy napisać nasze funkcje, aby były wolne od skutków ubocznych, nie ponosząc przy tym ogromnych kosztów.
Niezmienny napowietrzny
Teraz, pod względem koncepcyjnym, modyfikowalne wersje zawsze będą miały przewagę wydajności. Zawsze istnieje taki narzut obliczeniowy związany z niezmiennymi strukturami danych. Ale uznałem, że jest to warta wymiany w przypadkach, które opisałem powyżej, i możesz skupić się na tym, aby koszty ogólne były wystarczająco minimalne. Wolę takie podejście, w którym poprawność staje się łatwa, a optymalizacja trudniejsza niż optymalizacja łatwiejsza, ale poprawność staje się trudniejsza. Nie jest to tak demoralizujące, że kod, który działa idealnie poprawnie, wymaga kilku ulepszeń w stosunku do kodu, który nie działa poprawnie w pierwszej kolejności, bez względu na to, jak szybko osiąga niepoprawne wyniki.