24 - a Vector monád¶
A funkcionális paradigmában a(z egyirányú, immutable láncolt) lista egy alapvető adatszerkezet.
Performance megfontolásból azonban mindenképp érdemes megismernünk más adatszerkezeteket is,
az egyik ilyen, mely szintén elemek egy immutable szekvenciáját tárolja, a Vector
lesz.
a problémák a Listtel¶
Lássuk először a megoldandó kérdéseket a listával:
- Egy listában nincs hatékony direkt címzés: ha az
n
-edik elemet akarjuk lekérni, akkor a lista fejétől el kell szaladnunkn
lépésen keresztül a megfelelő elemhez. Ez lassú tud lenni. - Egy immutable listában a módosítás nagyon nem hatékony. Mit is jelent ez? Mivel a lista immutable,
így egy olyen
updated(i: Int, value: T): List[T]
metódust keresünk, ami visszaad egy olyan új listát, mely ugyanaz, mint az eredeti, azzal az egy különbséggel, hogy a listai
. elemét felváltja avalue
elem.
Implementáljuk ezt a metódust a saját MyList
adatszerkezetünkön!
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Válasz mutatása
Mi ezzel a gond? Ha például a listánk az (1,4,2,8,5,7)
lista, akkor ez így van a memóriában:
1 2 3 |
|
head
, jobbra a tail
mező)
Ha ezen hívjuk az updated(3,6)
metódust, akkor a memóriában hogy fog kinézni az új listánk?
1 2 3 4 5 6 7 |
|
Válasz mutatása
Nem csak hogy az elért indexszel arányosan sok lépést teszünk meg az updated metódus hívásakor, de még ráadásul attól a ponttól kezdve visszafelé fel is építjük az egész listát, hiába változatlan!
Ez egyrészt sokáig tart, másrészt ha amúgy már máshol nincs szükségünk az eredeti listára, akkor ugyan idővel erre rájön a JVM és felszabadítja a memóriát, de elég nagy méretű memóriát foglalunk pluszban, ha hosszú lista vége felé módosítunk.
-
A
List
en nem igazán tudunk párhuzamos számításokat végezni: érzi az ember, hogy például egymap
-szerű metódust egy tömbön egyszerűen tudnánk párhuzamosítani, minden core egyszerre egy tömbelemet helyettesítene be a mappelő függvénybe; a listában viszont ahhoz, hogy elérjék az elemeket, követniük kell a linkeket. -
prepend függvénye lehet, de az append függvénnyel hasonlóak a problémák, mint az
updated
metódussal: újra kell hozzá másolni az egész listát.
a Vector¶
A Vector
osztály ezeket a hiányosságokat küszöböli ki:
- van direkt access:
apply(index: Int): T
metódus aVector[T]
-ben visszaadja azi
. indexen lévő elemet (mint egy C vagy Java tömbben,0
-tól indexel sorfolytonosan, ez a direkt access ,,gyors'' (nem annyira, mint a tömbcímzés, de majdnem) - van
updated(index: Int, value: T): Vector[T]
metódusa, ami ,,gyorsan'' updateli a megfelelő indexen lévő elemet - ez is immutable, tehát egy új Vectort kapunk vissza, de sokkal kevesebb adatot másol hozzá pluszban le, mint ami a listánál történik - van append, prepend
és
- ez is monád
használata¶
A listában is szereplő műveletei megegyeznek azéval, nem kell beimportolnunk semmit, mert ő is le van type aliasolva a Predef
objektumban,
teljes neve scala.collections.immutable.Vector
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Emlékeztetőül az appendhez és a prependhez: ha a metódus neve :
-ra végződik, akkor amennyiben nem tesszük ki a pontot, úgy balról ragad az objektumhoz,
így a 7 +: vec2
kifejezés a vec2.+:(7)
kifejezéssel egyenértékű.
felépítése a memóriában¶
Nem szerves része a kurzusnak az, hogy pl pont Scalában valamelyik adatszerkezezet hogyan van felépítve, de azért pár szó arról, hogy hogy néz ki ez az adatszerkezet:
- alapvetően amíg a
Vector
mérete32
vagy kevesebb, addig ez egy tömb lesz, konstans időben eléri az elemeit, azupdated
metódus lemásolja az egész tömböt - ha több, mint
32
eleme van, akkor egy fa struktúrában kezdi tárolni az elemeit: a fa gyökerében 32 darab referencia lesz egy tömbben, mindegyik referencia egy 32-elemű tömbre fog mutatni, így összesen32 * 32 = 1024
elem tárolható egy ilyen kétszintes fában - ha pl. ebben a
100
. elemet akarjuk elérni, akkor a100/32=3
azt mondja, hogy a3
. referenciát kell követnünk a tömbben (a nulladik tárolja a0..31
, az első a32..63
, a második a64..95
és a harmadik a96..127
. elemeket), majd ha oda leléptünk, akkor ott a100 % 32=4
azt mondja, hogy ebben a tömbben a4
. indexen lesz a keresett elem, így ha nem is konstans időben, de gyorsan elérjük - ha pl. updateljük a
100
. elemet, akkor csak az őt ténylegesen tároló32
-elemű tömb csomópontot kell lemásoljuk megváltoztatva a memóriában, és a gyökeret (mivel az tárol egy új mutatót), de ez csak2 * 32 = 64
elem másolása akkor is, ha aVector
már 1000 elemet tárol - ha eléri az 1024 helyet, akkor újra növeli eggyel a szintet, a felső két szinten
32
-elemű, referenciákat tároló tömbök lesznek, alul pedig a tényleges adatok, ez már32.768
méretig elég, és bárkit3
csomóponton keresztül elérünk, maximum3*32
méretű tömböt kell update-nél duplikálnunk - négy szint már egymillió elemre, hat szint már egymilliárd elemre elég
- nem teljesen így néz ki, hogy a prepend és az append is hatékony lehessen, tárol egy "offset"et is, hogy valójában hanyadik elemtől is kezdődik a tényleges adat
Alapvetően ha immutable sorozatot szeretnénk tárolni, a List
nél kb minden tekintetben jobb választás lesz real-life scenarióban a Vector
.
Általában ha n
elemet tárolunk a Vectorban, akkor hány szintes fánk lesz?
Válasz mutatása
for comprehension több monáddal¶
Vajon mi lesz a for( x <- list; y <- vec ) yield x*y
kifejezés értéke (ha lefordul egyáltalán), ha list=List(1,2,3)
és vec=Vector(1,5)
? És
a for( x <- vec; y <- list ) yield x*y
értéke?
1 2 3 4 5 6 |
|
1 |
|
1 |
|
Válasz mutatása
Mindenesetre, az látszik, hogy az a rész, hogy két különböző osztályból hogy is lesz egy eredménye a flatMap
nek, az nemtriviális, és nem mindig
van jóldefiniálva vagy megvalósítva: minél több monádot (vagy legalábbis flatMap
metódussal rendelkező osztályt) fogunk ismerni, annál többször futhatunk
bele abba, hogy nem minden enumerátor-kombináció fog lefordulni. Szerencsére a built-in osztályok közt vannak konverziós metódusok: a Vector
és a List
is rendelkezik toVector
és toList
metódusokkal, amik a másik konténerosztálynak megfelelő adatszerkezetbe rakják át az adatot, ez persze lineáris
ideig eltart.