Immutable vs mutable

tl;dr
Egyik korábbi blogomhoz érkezett egy ilyen hozzászólás:

Idézet:
"Eddig bármikor is vettem vmit automatából, sose lett új pénztárcám, és sose termett előttem egy másik automata."

Sokan vallják a fentieket, vagyis, hogy a valóságban nem képződnek új objektumok, hanem egy létező objektum állapota változik csak meg.
Ez a hagyományos OO szemlélet.
A másik szemlélet az FP szerinti, ami ellen a fenti idézet is irányul, vagyis, hogy ha változik egy objektum, akkor azt új objektumnak tekintem.

Sokan OO vs FP ellentétként élik meg, magyarázzák, de a fő gond nem az OO-val van, hanem a mutabilitással. Az OO-t lehet immutable-ként is használni, úgy sokkal kevesebb gond van vele(, illetve mutable-ként is, ha üzenetváltással kommunikálunk).
Ha már itt tartunk, akkor megjegyezném, hogy szerintem a következő programozási paradigmákat érdemes használni (csökkenő fontossági sorrendben), ha egyszerű, könnyen érthető, de hatékony programot szeretnénk készíteni:

  1. FP
  2. immutable OOP
  3. mutable (OOP) üzenetekkel (pl. akka, smalltalk)
  4. egyéb mutable

Kanyarodjunk csak vissza az OO szemlélethez, amit inkább mutable szemléletnek neveznék. Miért is nem jó ez, ha így működik a valóságban is?
Egyrészt a valóságban sem így működik, ez csak egy leegyszerűsített modell. A valóságban sejtek, atomok, quarkok vannak, amik folyamatosan változnak (pl. kvázi 7 évente az emberi sejtek nagy része lecserélődik).
Másrészt nézzünk meg egy kis gondolat kísérletet! Természetesen, itt a HUP-on kötelezően, autós példával. Ha már minden példa rossz, akkor miért ne legyen autós? :)
Vegyünk egy autót (A). Ha átfestjük, akkor ugyanaz marad, csak a színe változik meg, ha kicseréljük egy ajtaját, akkor is ugyanaz marad, csak egy ajtaja változott meg.
Ez utóbbi mentén szépen sorban cseréljük le az összes alkatrészét mondjuk 50 lépésben. Ekkor szintén elmondhatjuk, mint ahogy minden lépésben, hogy ugyanaz maradt, csak az összes alkatrésze cserélődött le. Tehát A autó ugyanaz, mint A^50. Ugyanaz, de még is mindenhol ott van egy csak (ezekkel a csak-okkal van baj, ezek testesítik meg az állapotokat).
A lecserélt alkatrészekből kezdjünk el építeni egy új autót. Az 50. lépés után lesz egy egész B autónk (B^50), ami pontosan azon alkatrészekből áll, mint a kiindulási A autónk, mégsem ugyanaz, mint az A, sőt az A^50 teljesen más mint az A, az A^50 mégis ugyanaz, mint az A.
Itt azért érzékelhetünk egy elég erős logikai ellentmondást.
Ha az A alkatrészeit egy C autó bontásából szerezzük, akkor A^50 megegyezik a C-vel, de mégsem ugyanaz (itt is egy ellentmondás), ha a lecserélt alkatrészt a C autóba illesztjük, akkor a végén A^50 megegyezik C-vel, mégsem ugyanaz, C^50 megegyezik A-val, mégsem ugyanaz (itt is van egy pár ellentmondás).
Egy szó, mint száz, ez is csak egy modell, és nem a valóság. Egy olyan modell, ami valamelyest modellezi a való dolgokat.

A másik modell, az FP szemléletű, amit inkább immutable szemléletnek neveznék (hiszen az OO számos alkalmazásával megfér).
Itt nem futunk bele ilyen logikai ellentmondásba, de nem is ezért jobb.

Akkor miért jobb az immutable szemlélet?

Nézzünk erre is egy (autós) példát!

Tegyük fel, hogy van egy modulunk, ami egy autót és egy személyt kap paraméterül, majd mindenféle funkciót valósít meg ezen személy és autó segítségével.
Az egyszerűség kedvéért tegyük fel, hogy

  • az autó a következő tulajdonságokkal bír: üres-e az autó, milyen a színe, mennyivel megy és hol van éppen,
  • van egy autópálya, amire a piros autók nem mehetnek fel,
  • személy tulajdonsága, hogy hol van éppen.

A megvalósítandó funkciókból három:

  • a) menjünk az autópályán 130-cal, fényképezzük le a km órát, majd twitteljük ki a képet,
  • b) fényképezzük le az autót piros színűként, majd twitteljük a képet,
  • c) mérjük meg az autónk súlyát, majd twitteljük a súlyát.

A modul paraméterezésekor ellenőrizzük, hogy az autó üres, áll, nem piros, az autó és a felhasználó helyzete megegyezik.
Ha hibás a paraméterezés, akkor a felhasználót tájékoztatjuk, hogy hibás adatokkal hívta meg a modulunkat.
Ezután hívogatjuk a funkcióinkat, sorban egymás után, vagy párhuzamosan.

Hogyan kell megírni a funkcióinkat két szemlélet esetén:
Immutable esetén:

  • a) Üljünk be az autóba, menjünk fel az autópályára, gyorsítsunk 130-ra, fényképezzük le a km órát, majd twitteljük ki a képet.
  • b) Fessük be az autót pirosra, fényképezzük le, majd twitteljük a képet.
  • c) Mérjük meg az autónk súlyát, majd twitteljük a súlyát.

Mutable esetén már sokkal nagyobb bajban vagyunk. Egyrészt mindenhol ellenőrizgetni kell, hogy az autó és a személy állapotai épp megfelelőek-e, ha nem megfelelő, akkor hibaüzenetet kell adjunk, de ezt kinek adjuk?, hiszen ezek program hibák, mert jól van paraméterezve a modul, csak az egyik funkció rossz állapotban hagyta az objektumunkat, pl. piros maradt a festés után, vagy épp 130-cal megy az autópályás funkció után, vagy benne ülünk, amikor mérnénk, így hibás eredményt kapnánk.
Ha párhuzamosan futtatnánk, akkor még egy rakat problémánk lesz az előzőeken túl. Pl. hiába ellenőrizzük, hogy éppen áll-e az autó, ha a következő pillanatban már megy, amikor be akarnánk szállni. Ezért mindenféle lock-okat kell az objektumainkra rakni, amiktől egyrészt sokkal lassabb lesz a futás (pl. a három másfél órás funkció, immutable esetén másfél óra alatt lemegy párhuzamosan, lock-olva, pedig kvázi sorban kell futniuk, így 4,5 óra alatt fog lefutni), másrészt deadlock és egyéb szinkronizációs problémák léphetnek fel.

A fentiek alapján már el tudjuk képzelni, hogy a mutable kódunk egyszerűség és olvashatóság szempontjából is sokkal rosszabb lesz, hiszen teli lesz if-ekkel, lock-ockal, try-catch-ekkel, amik mind hiányoznak az immutable kódból.

Hozzászólások

Nekem csak egy bajom van ezzel: ezek szerint barmilyen allapotvaltozas kotelezoen uj objektumot kell eredmenyezzen? Es mi van azokkal a cuccokkal, amik ugyan meg a regi objektumra tartalmaznak referenciat, de profitalhatnanak az uj allapotbol is?

Mondok egy peldat: ha az autot atfestjuk kekrol pirosra, de a kepkeszito modul a kek autorol oriz objektumot, akkor a kep attol fuggetlenul egy kek autot fog abrazolni, hogy amugy az auto mar piros. Magat a kepkeszites folyamatat erdemben nem befolyasolja (nem valtoztatja meg az algoritmus lefutasat), hogy az auto kek, piros, sarga, zold vagy lila, viszont profital abbol (pontosabb kepet keszit) hogy az uj allapot a regi objektumon lep ervenybe.

Ugyankkor felmerul a kerdes, hogy hogyan frissitjuk az uj objektumra a referenciakat? Hiszen nem regisztralhatjuk, hogy kik kertek le az objektumot, plane nem feltetlen nyalhatunk vissza emiatt, hogy frissitsuk a referenciakat.
--
Blog | @hron84
Üzemeltető macik

"Nekem csak egy bajom van ezzel: ezek szerint barmilyen allapotvaltozas kotelezoen uj objektumot kell eredmenyezzen?"
Igen.

"Es mi van azokkal a cuccokkal, amik ugyan meg a regi objektumra tartalmaznak referenciat, de profitalhatnanak az uj allapotbol is?"
Át kell adni nekik az új objektumot, és úgy kell kérni kép készítést.

"regi objektumra tartalmaznak referenciat,", "kik kertek le az objektumot,"
Ebben a modellben megmondjuk, hogy mit csinálj és azt mivel, nem azt hogy csináld és kérd le hozzá az adatokat.
Ez FP-nél nyilván így van, hiszen mindig egy függvényt hívsz és átadod a paramétereket. Pl. makePicture(car)
Hasonlóan működhet ez OO-nál is, csak ott az objektumnak adod át, hogy mivel csinálja, aztán csináltatod, a lényeg, hogy az átadott adatok (objektumok) sem változnak meg, újat kell átadni, hogy ha az újjal akarod, hogy dolgozzon.
TellDontAsk

"Át kell adni nekik az új objektumot, és úgy kell kérni kép készítést."

Ez multithread kornyezetben lehet maceras. Marmint, ha idoben elobb kered a kep keszitest, mint a szinvaltast, am maga a kep elkeszulese megis idoben a szinvaltas utan tortenik meg (akarmiert), akkor a kepkeszites ettol meg profitalhatna a szinvaltasbol, meg ha magat a kepkeszitest amugy nem befolyasolja, hogy milyen szinu auto lesz a kepen.

"Hasonlóan működhet ez OO-nál is, csak ott az objektumnak adod át, hogy mivel csinálja, aztán csináltatod, a lényeg, hogy az átadott adatok (objektumok) sem változnak meg, újat kell átadni, hogy ha az újjal akarod, hogy dolgozzon."

Igen, csak ez esetben tok felesleges lenne az OO. Marmint, ez esetben a muveletet vegzo "osztalyok" gyakorlatilag semmi masok, mint statikus fuggvenyek tarhaza, hiszen nem tarolhatnak allapotot magukrol, a this-bol ugyanis nem tudsz ujat gyartani egy allapotvaltozasnal.

Ertem, hogy hol vannak ennek a megkozelitesnek a benefitjei, csak valahol azt gondolom, hogy nem ordogtol valo az, ha egy objektum belso allapota megvaltozhat. Nem azt mondom, hogy mindig hasznos, azt mondom, hogy nem mindig karos. Ha ugyanis mindig karos volna, akkor okos emberek nem talaltak volna ki az OO-t ugy, hogy ez megis lehetseges legyen.
--
Blog | @hron84
Üzemeltető macik

"Ez multithread kornyezetben lehet maceras. ..."

Ez nekem nem tűnik sehol sem előnynek, hogy éppen ahogy puffan, úgy kapok valamilyen eredményt. Sokkal jobb, ha megvan mindennek a szabályozott medre, főleg multithread környezetben.

"Igen, csak ez esetben tok felesleges lenne az OO. Marmint, ez esetben a muveletet vegzo "osztalyok" gyakorlatilag semmi masok, mint statikus fuggvenyek tarhaza, hiszen nem tarolhatnak allapotot magukrol, a this-bol ugyanis nem tudsz ujat gyartani egy allapotvaltozasnal."

Egyrészről így van, másrészről készíthetnek új példányt magukból az állapotváltozásnak megfelelően.

"Nem azt mondom, hogy mindig hasznos, azt mondom, hogy nem mindig karos."

Pontosan ugyanezt mondom én is. A legtöbb esetben használj immutable dolgokat, amire nem érdemes, arra mutable-t.

Az egyes eseteket (ugyanaz, csak) leírva magad is rájöttél, csak nem írtad le, hogy mi a lényege az egésznek, csak nem mondtad ki:
"Ha az A alkatrészeit egy C autó bontásából szerezzük, akkor A^50 megegyezik a C-vel, de mégsem ugyanaz (itt is egy ellentmondás), ha a lecserélt alkatrészt a C autóba illesztjük, akkor a végén A^50 megegyezik C-vel, mégsem ugyanaz, C^50 megegyezik A-val, mégsem ugyanaz (itt is van egy pár ellentmondás)."
Az egész lényege az object identity. Az, hogy két objektum mikor egyenlő, és mikor nem, az a modelltől függ. És van olyan modell, ami megengedi, hogy bizonyos objektumtulajdonságokat ne vegyünk figyelembe az egyenlőség vizsgálatánál.
Például két File objektum bizonyos modellekben egyenlő, ha ugyanarra az inode-ra mutatnak, attól függetlenül, hogy a File-nak mi a tartalma.

Az egész csak arról szól, hogy minden modellben szükséges-e, hogy az adott objektum minden tulajdonsága szerepet kapjon az objektumegyenlőség vizsgálatánál. Van olyan modell, ahol nem kap szerepet (például a járműnyilvántartás modellezésénél tök mindegy, hogy az autónak mi a színe, mégha nyilvántartod is, nem számít az objektumegyenlőségbe).

Így nem igaz az, hogy az immutable modellek mindig jobban, mint a mutable modellek. Csak arról van szó, hogy minden property kell-e az objektumegyenlőséghez, vagy nem.
Simán előfordulhat két objektumra, hogy egyik szempont szerint egyenlőek, másik szerint nem, ezt pedig csak és kizárólag a valóságban elfoglalt szerep (amit modellezünk), határoz meg, és nem funkcionális szemlélet, vagy más egyéb megközelítés. Mindig a felhasználás szerinti egyenlőségfogalom számít, és lehet, hogy két objektumra többféle egyenlőségfogalom (pontosabban ekvivalenciareláció) van definiálva.

A matematikai objektumok pedig azért eleve immutable objektumok, mert ott számít minden property (illetve ez sem teljesen igaz, mert ugye a homomorfizmusok szintjén egyenlő objektumokat azonosaknak szoktak tekinteni).

Az, hogy lockok kellenek, vagy nem kellenek, az a valóság modellezése szempontjából lényegtelen: ha a valóságban is vannak lockok, akkor a programban is kellenek.

Azt hiszem kevered az immutable-t az objektum egyenlőséggel.

Az (im)mutable azt jelenti, hogy ugyanaz a referenciája és közben (nem) módosulhat az értéke.
Az objektum egyezőség pedig az, ha két eltérő referenciájú objektumot hasonlítunk, hogy ugyanaz-e az értéke.

Immutable és mutable esetén is lehet olyan, hogy két eltérő referenciájú objektum egyenlő és olyan is, hogy nem egyenlő.
Mutable esetén lehet olyan, hogy egy objektum egyik időpillanatban nem egyezik meg egy másik időpillanatban levő értékével, ami immutable esetén nem lehetséges, ott ahogy létrejött az objektum onnantól mindig ugyanaz marad. (változó vs konstans)

"Így nem igaz az, hogy az immutable modellek mindig jobban, mint a mutable modellek."

Ilyet nem is állítottam. Sok esetben a mutable modellek jobbak, pl. teljesítmény vagy memóriafelhasználás szempontjából, de ezek speciális esetek, én csak annyit állítok, hogy általában jobbak az immutable modellek.

"Az, hogy lockok kellenek, vagy nem kellenek, az a valóság modellezése szempontjából lényegtelen: ha a valóságban is vannak lockok, akkor a programban is kellenek."

Ezt nem igazán értem, hogy milyen valóságos lock-ra gondolsz, de immutable esetén sohasem kell lock, és mindent meg lehet írni immutable-ként is.

" mindent meg lehet írni immutable-ként is."

Azert ez nem valid erv, mert persze, mindent meg lehet irni Assemblyben is, kerdes, hogy megeri-e. Marmint, az az effort, amit arra koltesz, hogy mindent feltetlenul immutable-kent irj meg, az nem tobb-e, mintha egyszeruen megvizsgalnad, hogy az adott esetben nem egyszerusit-e rengeteget a kodon az, ha mutable objektumokat hasznalsz.

Az a feltetelezes ugyanis hamis, hogy minden egyes valtozas eseten lockok kellenek. Az en meglatasom szerint akkor van hasznuk az immutable objektumoknak, HA amugy korbe kellene lockolni a valtozasok ellen az erintett objektumot. Ha azonban az algoritmus ezt nem koveteli meg (marpedig eleg sokszor nem koveteli meg, mert pl. barmilyen bemenetre kozel hasonlo muveleteket hajt vegre, legfeljebb a kimenet fog egy picit valtozni a bemenet fuggvenyeben), akkor az immutable objektumokkal lejtett kortancok tokeletesen feleslegesek es csak arra jok, hogy a GC-nek lehetoleg minel tobb dolga legyen.
--
Blog | @hron84
Üzemeltető macik

Ha kiragadsz egy fél mondatot azzal nem igazán tudok mit kezdeni.
Nem azt írtam, amit idézel! Ott egy teljes mondat van, ami arról szól, hogy nem gondolom úgy, hogy ahol a valóságban kell lock (bármit jelentsen is ez), ott a kódban is kell.

Sőt kifejezetten azt írtam kicsit előrébb, hogy sok esetben szerintem is jobb a mutable, de általában nem.

Ha arra a mondatra szeretnél reagálni, akkor írj egy példát olyan "valóságbeli lockra", ami mindenképp kell még immutable esetén is.

"Az a feltetelezes ugyanis hamis, hogy minden egyes valtozas eseten lockok kellenek."

Ilyet sem írtam.

Ne buzgalkodjatok!
Tul sokat buzgalkodotok.

Én minden divatos TLA-t támogatok és helyeslek, függetlenül attól, hogy jól ismert dolog újracsomagolása-e, vagy szimpla butaság, de most ezzel a 'FP'-vel kapcsolatban az az érzésem, hogy ez egy sikerületlen TLA.

Milyen nyelven szeretsz FP szemlélettel programozni?

Scala az elsődleges, de igazából a szemlélet a fontos.

Igyekszem minden nyelven a lehető leginkább élni vele, de sok-sok OOP után elég nehéz átállni.
Napi szinten a Java és a Javascript amit használok, ez utóbbit kevésbé, bár ezt jobb szeretem a kettő közül.
Utóbbi kettő szépen nyit az FP felé, így egyre jobb érzés ezekben is programozni.

:) Java-ban és Javascript-ben valóban csak a felületet lehet kapargatni, az "igaz FP"-nek sajnos nem lehet a közelébe jutni. Bár az ES6 kicsit bővít a lehetőségeken.

(Szerk.: Arról nem is beszélve, hogy vannak könyvtárak, amik további segítséget adnak, mint Java-nal a TotallyLazy, vagy Javascript-nél az Immutablejs)

Szerintem azért fontos, hogy magát a szemléletet minél inkább alkalmazzuk, már amennyire egy nyelv engedi, még akkor is, ha nem kifejezetten FP nyelvről is van szó.

Mint hithu immutability rajongo, subscribe. A mutable monnjon le, mert azzal csak a baj van.

--
|8]