Dlaczego niezmienność jest tak ważna (lub potrzebna) w JavaScript?

205

Obecnie pracuję nad platformami React JS i React Native . W połowie drogi natknąłem się na bibliotekę Immutability lub Immutable-JS , kiedy czytałem o implementacji Flux i Redux na Facebooku.

Pytanie brzmi: dlaczego niezmienność jest tak ważna? Co jest złego w mutowaniu obiektów? Czy to nie ułatwia rzeczy?

Podając przykład, rozważmy prostą aplikację do czytania Wiadomości, której ekran początkowy stanowi widok listy nagłówków wiadomości.

Jeżeli ustawić powiedzieć o tablicę obiektów o wartości początkowo nie mogę manipulować. Tak mówi zasada niezmienności, prawda? (Popraw mnie, jeśli się mylę.) Ale co, jeśli mam nowy obiekt Wiadomości, który należy zaktualizować? W zwykłym przypadku mogłem właśnie dodać obiekt do tablicy. Jak mogę osiągnąć w tym przypadku? Usunąć sklep i odtworzyć go? Czy dodanie obiektu do tablicy nie jest tańszą operacją?

bozzmob
źródło
2
Niezmienna struktura danych i czysta funkcja prowadzą do przezroczystości referencyjnej, co znacznie ułatwia rozumowanie zachowania twojego programu. W przypadku korzystania z funkcjonalnej struktury danych otrzymujesz również możliwość cofania za darmo.
WorBlux,
Podałem punkt widzenia Redux @bozzmob.
prosti
1
Przydatne może być poznanie ogólnej niemożności unieruchomienia jako koncepcji funkcjonalnego paradygmatu, zamiast próbować myśleć, że JS ma z tym coś wspólnego. React jest napisany przez fanów programowania funkcjonalnego. Musisz wiedzieć, co wiedzą, aby je zrozumieć.
Gherman
Nie jest to konieczne, ale oferuje kilka dobrych kompromisów. Zmienny stan dotyczy oprogramowania, podobnie jak ruchome części
Kristian Dupont,

Odpowiedzi:

196

Ostatnio badam ten sam temat. Zrobię co w mojej mocy, aby odpowiedzieć na twoje pytanie (pytania) i postaram się podzielić tym, czego nauczyłem się do tej pory.

Pytanie brzmi: dlaczego niezmienność jest tak ważna? Co jest złego w mutowaniu obiektów? Czy to nie ułatwia rzeczy?

Zasadniczo sprowadza się to do tego, że niezmienność zwiększa przewidywalność, wydajność (pośrednio) i pozwala na śledzenie mutacji.

Przewidywalność

Mutacja ukrywa zmiany, które powodują (nieoczekiwane) skutki uboczne, które mogą powodować paskudne błędy. Wymuszając niezmienność, możesz uprościć architekturę aplikacji i model mentalny, co ułatwia rozumowanie aplikacji.

Występ

Mimo że dodawanie wartości do niezmiennego obiektu oznacza, że ​​należy utworzyć nową instancję, w której należy skopiować istniejące wartości i dodać nowe wartości do nowego obiektu, które kosztują pamięć, niezmienne obiekty mogą korzystać ze współdzielenia strukturalnego w celu zmniejszenia pamięci nad głową.

Wszystkie aktualizacje zwracają nowe wartości, ale struktury wewnętrzne są współużytkowane, aby drastycznie zmniejszyć zużycie pamięci (i thrashing GC). Oznacza to, że jeśli dodasz do wektora zawierającego 1000 elementów, tak naprawdę nie tworzy on nowego wektora o długości 1001 elementów. Najprawdopodobniej wewnętrznie przydzielonych jest tylko kilka małych obiektów.

Możesz przeczytać więcej na ten temat tutaj .

Śledzenie mutacji

Poza zmniejszonym zużyciem pamięci, niezmienność pozwala zoptymalizować twoją aplikację poprzez wykorzystanie równości odniesienia i wartości. Dzięki temu naprawdę łatwo sprawdzić, czy coś się zmieniło. Na przykład zmiana stanu w składniku reagującym. Możesz użyć, shouldComponentUpdateaby sprawdzić, czy stan jest identyczny, porównując obiekty stanowe i zapobiec niepotrzebnemu renderowaniu. Możesz przeczytać więcej na ten temat tutaj .

Dodatkowe zasoby:

Jeśli ustawię, powiedzmy początkowo tablicę obiektów o wartości. Nie mogę tego manipulować. Tak mówi zasada niezmienności, prawda? (Popraw mnie, jeśli się mylę). Ale co, jeśli mam nowy obiekt Wiadomości, który należy zaktualizować? W zwykłym przypadku mogłem właśnie dodać obiekt do tablicy. Jak mogę osiągnąć w tym przypadku? Usunąć sklep i odtworzyć go? Czy dodanie obiektu do tablicy nie jest tańszą operacją?

Tak, to jest poprawne. Jeśli nie masz pewności, jak zaimplementować to w swojej aplikacji, polecam przyjrzeć się, jak robi to redux , aby zapoznać się z podstawowymi koncepcjami, bardzo mi pomogło.

Lubię używać Redux jako przykładu, ponieważ obejmuje niezmienność. Ma jedno niezmienne drzewo stanów (określane jako store), w którym wszystkie zmiany stanu są jawne poprzez wysyłanie akcji przetwarzanych przez reduktor, który akceptuje poprzedni stan wraz ze wspomnianymi akcjami (pojedynczo) i zwraca następny stan aplikacji . Możesz przeczytać więcej o jego podstawowych zasadach tutaj .

Istnieje świetny kurs redux na egghead.io, w którym Dan Abramov , autor redux, wyjaśnia te zasady w następujący sposób (nieco zmodyfikowałem kod, aby lepiej pasował do scenariusza):

import React from 'react';
import ReactDOM from 'react-dom';

// Reducer.
const news = (state=[], action) => {
  switch(action.type) {
    case 'ADD_NEWS_ITEM': {
      return [ ...state, action.newsItem ];
    }
    default: {
        return state;
    }
  }
};

// Store.
const createStore = (reducer) => {
  let state;
  let listeners = [];

  const subscribe = (listener) => {
    listeners.push(listener);

    return () => {
      listeners = listeners.filter(cb => cb !== listener);
    };
  };

  const getState = () => state;

  const dispatch = (action) => {
    state = reducer(state, action);
    listeners.forEach( cb => cb() );
  };

  dispatch({});

  return { subscribe, getState, dispatch };
};

// Initialize store with reducer.
const store = createStore(news);

// Component.
const News = React.createClass({
  onAddNewsItem() {
    const { newsTitle } = this.refs;

    store.dispatch({
      type: 'ADD_NEWS_ITEM',
      newsItem: { title: newsTitle.value }
    });
  },

  render() {
    const { news } = this.props;

    return (
      <div>
        <input ref="newsTitle" />
        <button onClick={ this.onAddNewsItem }>add</button>
        <ul>
          { news.map( ({ title }) => <li>{ title }</li>) }
        </ul>
      </div>
    );
  }
});

// Handler that will execute when the store dispatches.
const render = () => {
  ReactDOM.render(
    <News news={ store.getState() } />,
    document.getElementById('news')
  );
};

// Entry point.
store.subscribe(render);
render();

Ponadto filmy te pokazują bardziej szczegółowo, jak osiągnąć niezmienność dla:

Danillouz
źródło
1
@naomik dzięki za opinie! Moim zamiarem było zilustrowanie tej koncepcji i wyraźne pokazanie, że Obiekty nie są mutowane i niekoniecznie pokazanie, jak je w pełni wdrożyć. Jednak mój przykład może być nieco mylący, za chwilę go zaktualizuję.
danillouz,
2
@bozzmob nie ma za co! Nie, to nie jest poprawne, musisz samodzielnie wymusić niezmienność w reduktorze. Oznacza to, że możesz stosować strategie przedstawione w filmach lub korzystać z biblioteki takiej jak immutablejs. Możesz znaleźć więcej informacji tutaj i tutaj .
danillouz
1
@ anomik ES6 constnie dotyczy niezmienności. Mathias Bynens napisał o tym świetny artykuł na blogu .
Lea Rosema,
1
@terabaud dzięki za udostępnienie linku. Zgadzam się, że to ważne rozróżnienie. ^ _ ^
Dziękuję
4
Wyjaśnij to: „Mutacja ukrywa zmianę, która powoduje (nieoczekiwane) skutki uboczne, które mogą powodować paskudne błędy. Gdy wymuszasz niezmienność, możesz zachować prostotę architektury aplikacji i modelu mentalnego, co ułatwia rozumowanie aplikacji”. Ponieważ nie jest to wcale prawdą w kontekście JavaScript.
Pavle Lekic
142

Przeciwny pogląd na niezmienność

TL / DR: Niezmienność jest bardziej modnym trendem niż koniecznością w JavaScript. Jeśli używasz React, zapewnia to porządne obejście niektórych mylących wyborów projektowych w zarządzaniu stanem. Jednak w większości innych sytuacji nie doda wystarczającej wartości nad złożonością, którą wprowadza, służąc więcej do uzupełnienia CV niż do zaspokojenia faktycznej potrzeby klienta.

Długa odpowiedź: przeczytaj poniżej.

Dlaczego niezmienność jest tak ważna (lub potrzebna) w javascript?

Cóż, cieszę się, że zapytałeś!

Jakiś czas temu bardzo utalentowany facet o imieniu Dan Abramov napisał bibliotekę zarządzania stanem javascript o nazwie Redux, która wykorzystuje czyste funkcje i niezmienność. Nakręcił także kilka naprawdę fajnych filmów , dzięki którym pomysł był naprawdę łatwy do zrozumienia (i sprzedaży).

Czas był idealny. Nowość Angulara zanikała, a świat JavaScript był gotowy skupić się na najnowszej rzeczy, która miała odpowiedni stopień fajności, a ta biblioteka była nie tylko innowacyjna, ale idealnie pasowała do React, który był sprzedawany przez inną potęgę w Dolinie Krzemowej .

Choć może to być smutne, w świecie JavaScript rządzą mody. Teraz Abramow zostaje okrzyknięty półbogiem i wszyscy, zwykli śmiertelnicy, musimy poddać się Tao Niezmienności ... Czy to ma sens, czy nie.

Co jest złego w mutowaniu obiektów?

Nic!

W rzeczywistości programiści mutowali obiekty dla er ... tak długo, jak istnieją obiekty do mutacji. Innymi słowy, ponad 50 lat rozwoju aplikacji.

A po co komplikować? Kiedy masz obiekt cati umiera, czy naprawdę potrzebujesz sekundy, cataby śledzić zmianę? Większość ludzi powiedziałaby cat.isDead = truei skończyła.

Czy (mutowanie obiektów) nie ułatwia rzeczy?

TAK! .. Oczywiście, że tak!

Szczególnie w JavaScript, który w praktyce jest najbardziej przydatny do renderowania widoku stanu, który jest utrzymywany gdzie indziej (np. W bazie danych).

Co się stanie, jeśli mam nowy obiekt Wiadomości, który należy zaktualizować? ... Jak mogę osiągnąć w tym przypadku? Usunąć sklep i odtworzyć go? Czy dodanie obiektu do tablicy nie jest tańszą operacją?

Możesz przejść do tradycyjnego podejścia i zaktualizować Newsobiekt, aby zmieniła się twoja reprezentacja tego obiektu w pamięci (i widok wyświetlany użytkownikowi, a przynajmniej można mieć nadzieję) ...

Lub alternatywnie ...

Możesz wypróbować seksowne podejście FP / Immutability i dodać swoje zmiany do Newsobiektu do tablicy śledzącej każdą historyczną zmianę, aby następnie iterować tablicę i dowiedzieć się, jaka powinna być poprawna reprezentacja stanu (uff!).

Próbuję dowiedzieć się, co jest tutaj. Proszę, oświeć mnie :)

Mody przychodzą i odchodzą, kolego. Istnieje wiele sposobów na skórowanie kota.

Przykro mi, że musicie znosić ciągle zmieniający się zestaw paradygmatów programowania. Ale hej, WITAMY W KLUBIE !!

Teraz kilka ważnych punktów do zapamiętania w odniesieniu do Niezmienności, a dostaniesz je w gorączkową intensywność, którą może zdobyć tylko naiwność.

1) Niezmienność jest świetna do unikania warunków wyścigowych w środowiskach wielowątkowych .

Środowiska wielowątkowe (takie jak C ++, Java i C #) są winne praktyki blokowania obiektów, gdy więcej niż jeden wątek chce je zmienić. Jest to niekorzystne z punktu widzenia wydajności, ale lepsze niż alternatywa uszkodzenia danych. A jednak nie tak dobre, jak uczynić wszystko niezmiennym (chwała Haskellowi!).

ALE NIESTETY! W JavaScript zawsze działasz na jednym wątku . Nawet pracownicy sieci (każdy działa w osobnym kontekście ). Ponieważ więc nie możesz mieć powiązanego z wątkiem stanu wyścigu w kontekście wykonywania (wszystkie te piękne zmienne globalne i zamknięcia), główny punkt na korzyść Niezmienności wychodzi poza okno.

(Mimo, że nie jest zaletą przy użyciu czystych funkcji pracowników internetową, która jest, że będziesz mieć żadnych oczekiwań dotyczących błahy z obiektami na głównym wątku).

2) Niezmienność może (jakoś) uniknąć warunków wyścigu w stanie twojej aplikacji.

Oto prawdziwy sedno sprawy, większość programistów (React) powie ci, że Immutability i FP mogą w jakiś sposób działać na magii, która pozwala na przewidywalność stanu twojej aplikacji.

Oczywiście nie oznacza to, że możesz uniknąć warunków wyścigu w bazie danych , aby to zrobić, musisz koordynować wszystkich użytkowników we wszystkich przeglądarkach , a do tego potrzebujesz technologii push back-end, takiej jak WebSockets ( więcej na ten temat poniżej), które będą rozpowszechniać zmiany wśród wszystkich użytkowników aplikacji.

Nie oznacza to również, że istnieje jakiś nieodłączny problem w JavaScript, w którym stan twojej aplikacji wymaga niezmienności, aby stać się przewidywalnym, każdy programista, który kodował aplikacje frontowe, zanim React powie ci o tym.

To dość mylące twierdzenie oznacza po prostu, że dzięki React stan zgłoszenia stanie się bardziej podatny na warunki wyścigowe , ale ta niezmienność pozwala ci usunąć ten ból. Czemu? Ponieważ React jest wyjątkowy .. został zaprojektowany jako wysoce zoptymalizowana biblioteka renderująca z spójnym zarządzaniem stanem zajmującym drugie miejsce, a zatem stanem komponentu zarządza się poprzez asynchroniczny łańcuch zdarzeń (inaczej „jednokierunkowe wiązanie danych”), nad którymi nie masz kontroli i polegaj na tym, że pamiętasz, aby nie mutować stanu bezpośrednio ...

Biorąc pod uwagę ten kontekst, łatwo jest zauważyć, że potrzeba niezmienności ma niewiele wspólnego z JavaScriptem, a wiele z warunkami wyścigu w React: jeśli masz kilka wzajemnie zależnych zmian w swojej aplikacji i nie ma łatwego sposobu, aby dowiedzieć się, co twój stan jest obecnie w stanie, będziesz się mylić , a zatem nie ma sensu używać niezmienności do śledzenia każdej historycznej zmiany .

3) Warunki wyścigu są kategorycznie złe.

Mogą być, jeśli używasz React. Ale są rzadkie, jeśli wybierzesz inną strukturę.

Poza tym zwykle masz znacznie większe problemy do rozwiązania… Problemy takie jak piekło uzależnienia. Jak rozdęta baza kodu. Jak twój CSS nie ładuje się. Jak powolny proces kompilacji lub utknięcie w monolitycznym zapleczu, który sprawia, że ​​iteracja jest prawie niemożliwa. Jak niedoświadczeni deweloperzy, którzy nie rozumieją, co się dzieje i robią bałagan.

Wiesz. Rzeczywistość. Ale hej, kogo to obchodzi?

4) Niezmienność wykorzystuje typy referencyjne w celu zmniejszenia wpływu na śledzenie każdej zmiany stanu.

Ponieważ poważnie, jeśli zamierzasz kopiować rzeczy za każdym razem, gdy zmienia się twój stan, lepiej upewnij się, że jesteś inteligentny.

5) Niezmienność umożliwia cofanie operacji .

Ponieważ… to jest najważniejsza funkcja, o którą poprosi kierownik projektu, prawda?

6) Niezmienny stan ma wiele fajnych możliwości w połączeniu z WebSockets

Wreszcie, akumulacja delt stanu stanowi dość interesujący przypadek w połączeniu z WebSockets, co pozwala na łatwe wykorzystanie stanu jako przepływu niezmiennych zdarzeń ...

Gdy grosz spadnie na tę koncepcję (stan jest ciągiem wydarzeń - a nie prymitywny zestaw zapisów przedstawiających najnowszą perspektywę), niezmienny świat staje się magicznym miejscem do zamieszkania. Kraina zdarzeniami pozyskiwane zdumienia i możliwości, które wykracza poza sam czas . I kiedy zrobione dobrze to na pewno może robić w czasie rzeczywistym aplikacje easi er osiągnąć, po prostu nadawać przepływ zdarzeń dla wszystkich zainteresowanych, aby mogli budować własną reprezentację teraźniejszości i odpisać swoje zmiany do strumienia komunalnego.

Ale w pewnym momencie budzisz się i zdajesz sobie sprawę, że cały ten cud i magia nie przychodzą za darmo. W przeciwieństwie do twoich chętnych współpracowników, twoi interesariusze (tak, ludzie, którzy ci płacą) mało dbają o filozofię lub modę, a dużo o pieniądze, które płacą, aby zbudować produkt, który mogą sprzedać. Najważniejsze jest to, że trudniej jest napisać niezmienny kod i łatwiej go złamać, a ponadto nie ma sensu mieć niezmiennego interfejsu, jeśli nie masz zaplecza do jego obsługi. Kiedy (i jeśli!) W końcu przekonasz swoich interesariuszy, że powinieneś publikować i konsumować wydarzenia za pomocą technologii push, takiej jak WebSockets, dowiadujesz się, jak trudno jest zwiększyć skalę produkcji .


Teraz kilka porad, jeśli zdecydujesz się je zaakceptować.

Wybór pisania JavaScript za pomocą FP / Immutability to także wybór, aby baza kodu aplikacji była większa, bardziej złożona i trudniejsza w zarządzaniu. Zdecydowanie argumentowałbym za ograniczeniem tego podejścia do reduktorów Redux, chyba że wiesz, co robisz ... A JEŚLI masz zamiar użyć niezmienności niezależnie od tego, następnie zastosuj stan niezmienności do całego stosu aplikacji , a nie tylko po stronie klienta, ponieważ inaczej tracisz jego prawdziwą wartość.

Teraz, jeśli masz szczęście, że możesz dokonywać wyborów w swojej pracy, spróbuj użyć swojej mądrości (lub nie) i rób to, co właściwe, przez osobę, która ci płaci . Możesz oprzeć to na swoim doświadczeniu, na jelitach lub na tym, co się wokół ciebie dzieje (co prawda, jeśli wszyscy używają React / Redux, istnieje uzasadniony argument, że łatwiej będzie znaleźć zasób, aby kontynuować pracę). Alternatywnie, możesz wypróbować metody Resume Driven Development lub Hype Driven Development . Mogą być bardziej twoją rzeczą.

Krótko mówiąc, niezmienność należy powiedzieć, że sprawi, że będziesz modna w stosunku do swoich rówieśników, przynajmniej do następnego szaleństwa, w którym to momencie będziesz zadowolony z przejścia.


Teraz po tej sesji autoterapii chciałbym zaznaczyć, że dodałem to jako artykuł na moim blogu => Niezmienność w JavaScript: pogląd kontrastyka . Nie wahaj się odpowiedzieć, jeśli masz silne uczucia, które chciałbyś również zdjąć z piersi;).

Steven de Salas
źródło
10
Cześć Steven, tak. Miałem wszystkie te wątpliwości, kiedy rozważałem immutable.js i redux. Ale twoja odpowiedź jest niesamowita! Dodaje wiele wartości i dziękuje za zajęcie się każdym punktem, w który miałem wątpliwości. Teraz jest o wiele wyraźniej / lepiej nawet po miesiącach pracy na niezmiennych obiektach.
bozzmob
5
Używam React z Flux / Redux od ponad dwóch lat i nie mogę się z tobą więcej zgodzić, świetna odpowiedź!
Pavle Lekic
6
Jestem bardzo podejrzliwy, że poglądy na niezmienność dość dobrze korelują z rozmiarem zespołu i bazy kodu, i nie sądzę, że to przypadek, że głównym orędownikiem jest gigant z doliny krzemowej. To powiedziawszy, z szacunkiem się nie zgadzam: niezmienność jest pożyteczną dyscypliną, tak jak niestosowanie goto jest pożyteczną dyscypliną. Lub testy jednostkowe. Lub TDD. Lub analiza typu statycznego. Nie oznacza to, że robisz je cały czas, za każdym razem (chociaż niektórzy tak robią). Powiedziałbym również, że użyteczność jest ortogonalna dla szumu: w matrycy przydatnych / zbędnych i seksownych / nudnych jest wiele przykładów każdego z nich. "hyped" !== "bad"
Jared Smith
4
Cześć @ftor, dobra uwaga, biorąc sprawy za daleko w innym kierunku. Ponieważ jednak istnieje mnóstwo artykułów i argumentów dotyczących „niezmienności w javascript”, czułem, że muszę to zrównoważyć. Tak więc początkujący mają przeciwny punkt widzenia, aby pomóc im w osądzeniu w obu przypadkach.
Steven de Salas
4
Informacyjny i wspaniale utytułowany. Dopóki nie znalazłem tej odpowiedzi, myślałem, że jako jedyny mam podobny pogląd. Zdaję sobie sprawę z wartości niezmienności, ale denerwuje mnie to, że stało się to dogmatem uciskającym wszystkie inne techniki (np. Ze szkodą dla wiązania 2-drogowego, które jest niezwykle przydatne do formatowania danych wejściowych, na przykład zaimplementowanego w KnockoutJS).
Tyblitz
53

Pytanie brzmi: dlaczego niezmienność jest tak ważna? Co jest złego w mutowaniu obiektów? Czy to nie ułatwia rzeczy?

W rzeczywistości jest odwrotnie: zmienność czyni sprawy bardziej skomplikowanymi, przynajmniej na dłuższą metę. Tak, ułatwia to początkowe kodowanie, ponieważ możesz po prostu zmieniać rzeczy gdziekolwiek chcesz, ale kiedy twój program się powiększy, staje się problemem - jeśli wartość się zmieniła, co ją zmieniło?

Gdy sprawisz, że wszystko będzie niezmienne, oznacza to, że danych nie można już zmienić z zaskoczenia. Wiesz na pewno, że jeśli przekażesz wartość do funkcji, nie można jej zmienić w tej funkcji.

Mówiąc prościej: jeśli używasz niezmiennych wartości, bardzo łatwo jest rozumować twój kod: każdy dostaje unikalną * kopię twoich danych, więc nie może z nią sflashować i złamać innych części twojego kodu. Wyobraź sobie, o ile ułatwia to pracę w środowisku wielowątkowym!

Uwaga 1: Niezmienność może potencjalnie kosztować wydajność w zależności od tego, co robisz, ale rzeczy takie jak Immutable.js optymalizują najlepiej, jak potrafią.

Uwaga 2: W mało prawdopodobnym przypadku, gdy nie byłeś pewien, Immutable.js i ES6 constoznaczają bardzo różne rzeczy.

W zwykłym przypadku mogłem właśnie dodać obiekt do tablicy. Jak mogę osiągnąć w tym przypadku? Usunąć sklep i odtworzyć go? Czy dodanie obiektu do tablicy nie jest tańszą operacją? PS: Jeśli przykład nie jest właściwym sposobem wyjaśnienia niezmienności, proszę dać mi znać, jaki jest właściwy praktyczny przykład.

Tak, twój przykład wiadomości jest całkowicie dobry, a twoje rozumowanie jest dokładnie słuszne: nie możesz po prostu zmienić swojej istniejącej listy, więc musisz utworzyć nową:

var originalItems = Immutable.List.of(1, 2, 3);
var newItems = originalItems.push(4, 5, 6);
TwoStraws
źródło
1
Nie zgadzam się z tą odpowiedzią, ale nie odnosi się ona do jego części „Chciałbym nauczyć się z praktycznego przykładu”. Można argumentować, że pojedyncze odniesienie do listy nagłówków wiadomości używanych w wielu obszarach jest dobrą rzeczą. „Muszę zaktualizować listę tylko raz, a wszystko, co odnosi się do listy wiadomości, jest aktualizowane za darmo” - myślę, że lepsza odpowiedź zajęłaby wspólny problem, jak przedstawił, i pokazałaby cenną alternatywę, która wykorzystuje niezmienność.
Dziękuję
1
Cieszę się, że odpowiedź była pomocna! Jeśli chodzi o twoje nowe pytanie: nie próbuj odgadnąć systemu :) W tym konkretnym przypadku coś, co nazywa się „dzieleniem strukturalnym”, dramatycznie redukuje szarpanie GC - jeśli masz 10 000 pozycji na liście i dodajesz 10 więcej, uważam, że niezmienne. js spróbuje ponownie wykorzystać poprzednią strukturę najlepiej, jak potrafi. Niech Immutable.js martwi się pamięcią, a szanse na to, że wyjdzie ona lepiej.
TwoStraws,
6
Imagine how much easier this makes working in a multi-threaded environment!-> Ok dla innych języków, ale nie jest to zaletą w jednowątkowym JavaScript.
Steven de Salas
1
@StevendeSalas zauważa, że ​​JavaScript jest przede wszystkim asynchroniczny i sterowany zdarzeniami. W ogóle nie jest odporny na warunki rasowe.
Jared Smith,
1
@JaredSmith nadal mój punkt pozostaje. FP i niezmienność są niezwykle użytecznymi paradygmatami, aby uniknąć uszkodzenia danych i / lub blokad zasobów w środowiskach wielowątkowych, ale nie w JavaScript, ponieważ jest to jednowątkowy. O ile nie brakuje mi jakiegoś świętego samorodka mądrości, głównym kompromisem tutaj jest to, czy jesteś gotów uczynić kod bardziej złożonym (i wolniejszym) w celu uniknięcia warunków wyścigu ... które są o wiele mniejszym problemem niż większość ludzi myśleć.
Steven de Salas,
37

Chociaż pozostałe odpowiedzi są w porządku, aby odpowiedzieć na twoje pytanie dotyczące praktycznego przypadku użycia (z komentarzy do innych odpowiedzi), pozwól na chwilę wyjść poza twój działający kod i spójrz na wszechobecną odpowiedź pod nosem: git . Co by się stało, gdybyś za każdym razem, gdy wciskałeś zatwierdzenie, nadpisywałeś dane w repozytorium?

Teraz mamy do czynienia z jednym z problemów, przed którymi stoją niezmienne kolekcje: nadęty pamięci. Git jest na tyle inteligentny, że nie tylko tworzy nowe kopie plików za każdym razem, gdy wprowadzasz zmiany, po prostu śledzi różnice .

Chociaż niewiele wiem o wewnętrznym działaniu git, mogę jedynie założyć, że używa strategii podobnej do bibliotek, do których się odwołujesz: współdzielenie strukturalne. Pod maską biblioteki używają prób lub innych drzew do śledzenia tylko różnych węzłów.

Strategia ta jest również dość wydajna w przypadku struktur danych w pamięci, ponieważ istnieją dobrze znane algorytmy operacji drzewa, które działają w czasie logarytmicznym.

Kolejny przypadek użycia: powiedz, że chcesz cofnąć przycisk w swojej aplikacji internetowej. W przypadku niezmiennych reprezentacji danych wdrożenie takich danych jest stosunkowo proste. Ale jeśli polegasz na mutacji, oznacza to, że musisz się martwić o buforowanie stanu świata i dokonywanie aktualizacji atomowych.

Krótko mówiąc, za niezmienność wydajności środowiska wykonawczego i krzywej uczenia się trzeba zapłacić. Ale każdy doświadczony programista powie ci, że czas debugowania przewyższa czas pisania kodu o rząd wielkości. Niewielkie zmniejszenie wydajności środowiska wykonawczego prawdopodobnie przeważają nad błędami związanymi ze stanem, których użytkownicy nie muszą znosić.

Jared Smith
źródło
1
Świetny przykład, który mówię. Moje rozumienie niezmienności jest teraz bardziej jasne. Dzięki Jared. Właściwie jedną z implementacji jest przycisk Cofnij: D I dla mnie uprościłeś sprawę.
bozzmob
3
Tylko dlatego, że wzór mający sens w git nie oznacza, że ​​to samo ma sens wszędzie. W Git zależy Ci na całej przechowywanej historii i chcesz łączyć różne gałęzie. Na froncie nie obchodzi cię większość historii stanu i nie potrzebujesz całej tej złożoności.
Ski
2
@Ski jest tylko skomplikowany, ponieważ nie jest domyślny. Zwykle nie używam mori lub immutable.js w moich projektach: zawsze waham się przed przyjęciem dep-stron innych firm. Ale gdyby to był domyślny (a la clojurescript) lub przynajmniej miał natywną opcję, korzystałbym z niego cały czas, ponieważ kiedy np. Programuję w clojure, nie od razu wszystko upycham w atomy.
Jared Smith,
Joe Armstrong powiedziałby: nie martw się wydajnością, poczekaj kilka lat, a prawo Moore'a zajmie się tym za ciebie.
ximo
1
@JaredSmith Masz rację, sprawy stają się coraz mniejsze i coraz bardziej ograniczone zasoby. Nie jestem jednak pewien, czy będzie to czynnik ograniczający JavaScript. Ciągle znajdujemy nowe sposoby na poprawę wydajności (na przykład Svelte). Nawiasem mówiąc, całkowicie zgadzam się z twoim drugim komentarzem. Złożoność lub trudność korzystania z niezmiennych struktur danych często sprowadza się do tego, że język nie ma wbudowanego wsparcia dla tej koncepcji. Clojure sprawia, że ​​niezmienność jest prosta, ponieważ jest upieczony w języku, cały język został zaprojektowany wokół idei.
ximo
8

Pytanie brzmi: dlaczego niezmienność jest tak ważna? Co jest złego w mutowaniu obiektów? Czy to nie ułatwia rzeczy?

O zmienności

Z technicznego punktu widzenia nie ma nic złego w zmienności. Jest szybki, ponownie wykorzystuje pamięć. Programiści są przyzwyczajeni od samego początku (jak pamiętam). Istnieje problem w stosowaniu zmienności i problemów, które może to przynieść.

Jeśli obiekt nie jest współdzielony z niczym, na przykład istnieje w zakresie funkcji i nie jest wystawiony na zewnątrz, trudno dostrzec korzyści w niezmienności. Naprawdę w tym przypadku nie ma sensu być niezmiennym. Poczucie niezmienności zaczyna się, gdy coś jest udostępniane.

Zmienność bólu głowy

Zmienna wspólna struktura może z łatwością stworzyć wiele pułapek. Każda zmiana w dowolnej części kodu z dostępem do odniesienia ma wpływ na inne części z widocznością tego odniesienia. Taki wpływ łączy wszystkie części razem, nawet jeśli nie powinny być świadome istnienia różnych modułów. Mutacja jednej funkcji może spowodować awarię zupełnie innej części aplikacji. To jest zły efekt uboczny.

Następnym często problemem z mutacją jest stan uszkodzony. Stan uszkodzony może się zdarzyć, gdy procedura mutacji zawiedzie w środku, a niektóre pola zostały zmodyfikowane, a niektóre nie.

Co więcej, przy mutacji trudno jest wyśledzić zmianę. Prosta kontrola referencji nie pokaże różnicy, aby wiedzieć, co zmieniło się, należy dokonać głębokiej kontroli. Aby monitorować zmianę, należy wprowadzić pewne obserwowalne wzorce.

Wreszcie mutacja jest przyczyną deficytu zaufania. Jak możesz być pewien, że jakaś struktura chciała wartości, jeśli można ją zmutować.

const car = { brand: 'Ferrari' };
doSomething(car);
console.log(car); // { brand: 'Fiat' }

Jak pokazuje powyższy przykład, przejście modyfikowalnej struktury zawsze może zakończyć się inną strukturą. Funkcja doSomething mutuje atrybut podany z zewnątrz. Brak zaufania do kodu, naprawdę nie wiesz, co masz i co będziesz miał. Wszystkie te problemy mają miejsce, ponieważ: Zmienne struktury reprezentują wskaźniki pamięci.

Niezmienność dotyczy wartości

Niezmienność oznacza, że ​​zmiany nie dokonuje się na tym samym obiekcie, strukturze, ale zmiana jest reprezentowana w nowym. A to dlatego, że odwołanie reprezentuje wartość nie tylko wskaźnika pamięci. Każda zmiana tworzy nową wartość i nie dotyka starej. Tak jasne reguły przywracają zaufanie i przewidywalność kodu. Z funkcji można bezpiecznie korzystać, ponieważ zamiast mutacji dotyczą one własnych wersji z własnymi wartościami.

Używanie wartości zamiast kontenerów pamięci daje pewność, że każdy obiekt reprezentuje określoną niezmienną wartość i można z niego bezpiecznie korzystać.

Niezmienne struktury reprezentują wartości.

Jeszcze bardziej nurkuję na ten temat w średnim artykule - https://medium.com/@macsikora/the-state-of-immutability-169d2cd11310

Maciej Sikora
źródło
6

Dlaczego niezmienność jest tak ważna (lub potrzebna) w JavaScript?

Niezmienność można śledzić w różnych kontekstach, ale najważniejsze jest śledzenie jej względem stanu aplikacji i interfejsu użytkownika aplikacji.

Rozważę wzorzec JavaScript Redux jako bardzo modne i nowoczesne podejście i dlatego, że o tym wspomniałeś.

W przypadku interfejsu użytkownika musimy uczynić go przewidywalnym . Będzie to przewidywalne, jeśli UI = f(application state).

Aplikacje (w JavaScript) zmieniają stan poprzez akcje realizowane za pomocą funkcji reduktora .

Funkcja reduktora po prostu przyjmuje akcję i stary stan i zwraca nowy stan, utrzymując stary stan nienaruszony.

new state  = r(current state, action)

wprowadź opis zdjęcia tutaj

Korzyścią jest to: podróżujesz w czasie po stanach, ponieważ wszystkie obiekty stanowe są zapisywane, i możesz odtąd renderować aplikację w dowolnym stanie UI = f(state)

Możesz więc łatwo cofać / powtarzać.


Zdarza się, że tworzenie wszystkich tych stanów wciąż może być wydajne pod względem pamięci, analogia z Gitem jest świetna, i mamy podobną analogię w systemie Linux z linkami symbolicznymi (opartymi na i-węzłach).

prosti
źródło
5

Kolejną zaletą niezmienności w Javascripcie jest to, że redukuje ono sprzężenie skroniowe, co ma znaczne zalety w zakresie projektowania. Rozważ interfejs obiektu za pomocą dwóch metod:

class Foo {

      baz() {
          // .... 
      }

      bar() {
          // ....
      }

}

const f = new Foo();

Może się zdarzyć, że wywołanie baz()jest wymagane, aby obiekt był w prawidłowym stanie, aby połączenie bar()działało poprawnie. Ale skąd to wiesz?

f.baz();
f.bar(); // this is ok

f.bar();
f.baz(); // this blows up

Aby to rozgryźć, musisz dokładnie zbadać wewnętrzne elementy klasy, ponieważ nie jest to od razu widoczne po badaniu interfejsu publicznego. Ten problem może eksplodować w dużej bazie kodu z dużą ilością zmiennych stanów i klas.

Jeśli Foojest niezmienny, nie stanowi to już problemu. Można bezpiecznie założyć, że możemy zadzwonić bazlub barw dowolnej kolejności, ponieważ wewnętrzny stan klasy nie może się zmienić.

widmowe dzieci
źródło
4

Pewnego razu był problem z synchronizacją danych między wątkami. Ten problem był wielkim bólem, było ponad 10 rozwiązań. Niektórzy próbowali rozwiązać to radykalnie. To było miejsce narodzin programowania funkcjonalnego. To jest tak jak marksizm. Nie mogłem zrozumieć, jak Dan Abramov sprzedał ten pomysł JS, ponieważ jest on jednowątkowy. On jest geniuszem.

Mogę podać mały przykład. W __attribute__((pure))gcc znajduje się atrybut . Kompilatory próbują ustalić, czy twoja funkcja jest czysta, czy nie, jeśli nie specjalnie ją odszyfrujesz. Twoja funkcja może być czysta, nawet twój stan jest zmienny. Niezmienność to tylko jeden z ponad 100 sposobów, aby zagwarantować, że Twoje funkcje będą czyste. W rzeczywistości 95% twoich funkcji będzie czystych.

Nie powinieneś używać żadnych ograniczeń (takich jak niezmienność), jeśli faktycznie nie masz poważnego powodu. Jeśli chcesz „cofnąć” jakiś stan, możesz tworzyć transakcje. Jeśli chcesz uprościć komunikację, możesz wysyłać zdarzenia z niezmiennymi danymi. To zależy od Ciebie.

Piszę tę wiadomość z republiki post marksizmu. Jestem pewien, że radykalizacja każdego pomysłu jest niewłaściwa.

puchu
źródło
Trzeci akapit ma wiele sensu. Dziękuję za to. „Jeśli chcesz„ cofnąć ”jakiś stan, możesz tworzyć transakcje” !!
bozzmob
Nawiasem mówiąc, porównanie do marksizmu można również przeprowadzić dla OOP. Pamiętasz Javę? Cholera, dziwne fragmenty Java w JavaScript? Hype nigdy nie jest dobry, powoduje radykalizację i polaryzację. Historycznie OOP był o wiele bardziej podekscytowany niż o Redux na Facebooku. Chociaż na pewno starali się jak najlepiej.
ximo
4

Inne podejście ...

Moja druga odpowiedź dotyczy tego pytania z bardzo praktycznego punktu widzenia i nadal mi się podoba. Postanowiłem dodać to jako kolejną odpowiedź, a nie dodatek do tej, ponieważ jest to nudna filozoficzna rant, która, mam nadzieję, również odpowiada na pytanie, ale tak naprawdę nie pasuje do mojej istniejącej odpowiedzi.

TL; DR

Nawet w małych projektach niezmienność może być przydatna, ale nie zakładaj, że ponieważ istnieje, jest przeznaczona dla Ciebie.

Znacznie dłuższa odpowiedź

UWAGA: dla celów tej odpowiedzi używam słowa „dyscyplina”, aby oznaczać samozaparcie dla pewnych korzyści.

Jest to podobne w formie do innego pytania: „Czy powinienem używać maszynopisu? Dlaczego typy są tak ważne w JavaScript?”. Ma również podobną odpowiedź. Rozważ następujący scenariusz:

Jesteś jedynym autorem i opiekunem bazy kodu JavaScript / CSS / HTML zawierającej około 5000 wierszy. Twój pół-techniczny szef czyta coś o Maszynowym-jak-nowy-upał i sugeruje, że możemy chcieć się do niego przenieść, ale pozostawia Ci decyzję. Więc czytasz o tym, bawisz się nim itp.

Masz teraz wybór, czy przejdziesz do maszynopisu?

Maszynopis ma kilka istotnych zalet: inteligencję, wczesne wykrywanie błędów, określanie interfejsów API z góry, łatwość naprawiania błędów podczas refaktoryzacji, mniej testów. Maszynopis ma również pewne koszty: pewne bardzo naturalne i poprawne idiomy JavaScript mogą być trudne do modelowania w jego niezbyt potężnym systemie typów, adnotacje zwiększają LoC, czas i wysiłek przepisywania istniejącej bazy kodu, dodatkowy krok w procesie kompilacji itp. Mówiąc bardziej ogólnie, tworzy podzbiór możliwych poprawnych programów JavaScript w zamian za obietnicę, że Twój kod będzie bardziej poprawny. Jest to arbitralnie restrykcyjne. O to chodzi: narzucasz dyscyplinę, która Cię ogranicza (mam nadzieję, że postrzelisz się w stopę).

Wracając do pytania, sformułowanego w kontekście powyższego akapitu: czy warto ?

W opisanym scenariuszu twierdziłbym, że jeśli jesteś dobrze zaznajomiony z bazą kodu JS dla małych i średnich firm, wybór użycia Maszynopisu jest bardziej estetyczny niż praktyczny. I to jest w porządku , nie ma nic złego w estetyce, po prostu niekoniecznie są przekonujące.

Scenariusz B:

Zmieniasz pracę i jesteś teraz programistą w firmie Foo Corp. Pracujesz z 10-osobowym zespołem na bazie kodu 90000 LoC (i wciąż rośnie) JavaScript / HTML / CSS z dość skomplikowanym procesem kompilacji obejmującym babel, webpack , pakiet wypełnień, reaguj na różne wtyczki, system zarządzania stanem, ~ 20 bibliotek stron trzecich, ~ 10 bibliotek wewnętrznych, wtyczki edytora, takie jak linijka z zasadami wewnętrznego przewodnika po stylu itp. itp.

Kiedy byłeś facetem / dziewczyną w LoK 5k, to nie miało tak wielkiego znaczenia. Nawet dokumentacja nie była aż tak wielką sprawą, nawet powrót do określonej części kodu po 6 miesiącach można było łatwo zrozumieć. Ale teraz dyscyplina jest nie tylko miła, ale konieczna . Ta dyscyplina może nie obejmować Typescript, ale prawdopodobnie będzie obejmować pewną formę analizy statycznej, a także wszystkie inne formy dyscypliny kodowania (dokumentacja, przewodnik po stylu, skrypty budowania, testy regresji, CI). Dyscyplina nie jest już luksusem , jest koniecznością .

Wszystko to miało zastosowanie GOTOw 1978 roku: twoja mała, czarna gra w blackjacka w C mogła używać GOTOlogiki s i spaghetti i po prostu nie było tak wielkim wyborem własnej przygody, ale wraz z rozwojem programów bardziej ambitne, no i niezdyscyplinowane użycie GOTOnie mogło być utrzymane. Wszystko to dotyczy dziś niezmienności.

Podobnie jak w przypadku typów statycznych, jeśli nie pracujesz na dużej bazie kodu z zespołem inżynierów, który ją utrzymuje / rozszerza, wybór zastosowania niezmienności jest bardziej estetyczny niż praktyczny: jego zalety wciąż istnieją, ale mogą jeszcze nie przewyższać kosztów.

Ale tak jak w przypadku wszystkich przydatnych dyscyplin, przychodzi moment, w którym nie jest już opcjonalny. Jeśli chcę utrzymać zdrową wagę, dyscyplina dotycząca lodów może być opcjonalna. Ale jeśli chcę być wyczynowym sportowcem, mój wybór tego, czy jeść lody, zależy od mojego wyboru celów. Jeśli chcesz zmienić świat za pomocą oprogramowania, niezmienność może być częścią tego, czego potrzebujesz, aby uniknąć jego zawalenia pod własnym ciężarem.

Jared Smith
źródło
1
+1 mi się podoba. Znacznie więcej na temat Jareda. A jednak niezmienność nie uratuje zespołu przed własnym brakiem dyscypliny. 😉
Steven de Salas
@StevendeSalas to forma dyscypliny. I jako taki, myślę, że jest skorelowany z (ale nie zastępuje) innymi formami dyscypliny inżynierii oprogramowania. Uzupełnia, a nie zastępuje. Ale jak powiedziałem w komentarzu do twojej odpowiedzi, wcale nie jestem zaskoczony, że jest popychany przez giganta technologicznego z grupą inżynierów walczących na tej samej ogromnej bazie kodu :) potrzebują oni całej dyscypliny, jaką mogą zdobyć. W przeważającej części nie mutuję obiektów, ale także nie używam żadnej formy wymuszenia, ponieważ, cóż, to tylko ja.
Jared Smith
0

Stworzyłem bibliotekę typu agnostic open source (MIT) dla mutowalnego (lub niezmiennego) stanu, która może zastąpić wszystkie te niezmienne pamięci, takie jak libs (redux, vuex itp.).

Niezmienne stany były dla mnie brzydkie, ponieważ było zbyt wiele do zrobienia (dużo akcji dla prostych operacji odczytu / zapisu), kod był mniej czytelny, a wydajność dużych zestawów danych nie do przyjęcia (ponowne renderowanie całego komponentu: /).

Za pomocą obserwatora stanu głębokiego mogę aktualizować tylko jeden węzeł z notacją kropkową i używać symboli wieloznacznych. Mogę również stworzyć historię stanu (cofnij / przywróć / podróż w czasie) zachowując tylko te konkretne wartości, które zostały zmienione {path:value}= mniejsze zużycie pamięci.

Dzięki obserwatorowi stanu głębokiego mogę precyzyjnie dostroić rzeczy i mam kontrolę ziarna nad zachowaniem komponentów, dzięki czemu można znacznie poprawić wydajność. Kod jest bardziej czytelny, a refaktoryzacja jest znacznie łatwiejsza - wystarczy wyszukać i zastąpić ciągi ścieżek (nie trzeba zmieniać kodu / logiki).

neuronet
źródło
-1

Myślę, że głównym powodem niezmiennych obiektów jest utrzymanie stanu tego obiektu.

Załóżmy, że mamy obiekt o nazwie arr. Ten obiekt jest ważny, gdy wszystkie elementy są tej samej litery.

// this function will change the letter in all the array
function fillWithZ(arr) {
    for (var i = 0; i < arr.length; ++i) {
        if (i === 4) // rare condition
            return arr; // some error here

        arr[i] = "Z";
    }

    return arr;
}

console.log(fillWithZ(["A","A","A"])) // ok, valid state
console.log(fillWithZ(["A","A","A","A","A","A"])) // bad, invalid state

jeśli arrstaniemy się niezmiennym obiektem, będziemy mieć pewność, że arr jest zawsze w poprawnym stanie.

bedorlan
źródło
Myślę, że arrfillWithZ
mutuje się za
jeśli użyjesz immutable.js, otrzymasz nową kopię obiektu za każdym razem, gdy go zmienisz. więc oryginalny obiekt pozostaje nietknięty
bedorlan