16 - map, filter, reduce, fold¶
Felmerül a kérdés, hogy mégis mikor érdemes valamit tagfüggvényként implementálni egy osztályon belül, és mikor rajta kívül.
Van, amikor nincs más választásunk, mint bent deklarálni (pl. amikor olyan adathoz fér hozzá a függvény, ami csak az osztályon belül látható
- a láthatósági módosítókról is lesz később poszt).
Mindenesetre, ha van az IntList
ünk, és egy alkalmazásban szeretnénk pl. egy függvényt, ami visszaad egy listát,
amiben minden elem kétszerese az eredeti listabelinek (tehát pl. az (1,4,2,8,5,7)
listán hívva a (2,8,4,16,10,14)
új listát adja vissza),
azt persze leimplementálhatjuk így is:
1 2 3 4 5 6 7 8 9 |
|
IntList
osztályunkat, ki lesz ajánlva egy ,,szorzás kettővel'' függvény,
amit jó eséllyel nem használna, mert csak egy bizonyos alkalmazásban egy bizonyos célra volt rá szükség.
Pláne, ha egy másik alkalmazásban épp kilenccel kell szorozni az elemeket, egy másikban hármat hozzáadni... és egy nagyon furcsa abomination kezd kifejlődni, ha ezt az utat követjük:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
map¶
Ez az a pont, ahol egy kicsit messzebbről szemlélve a kódot valami általánosabb mintát láthatunk benne, amivel elég csak egy függvényt implementálni: mivel funkcionális programozásban próbálunk gondolkodni, észrevehetjük, hogy itt valójában két dolog szerepel inputként: a lista és egy művelet, ami a lista elemeit transzformálja valahogy egyenként, függetlenül. Az első esetben a kettővel szorzás függvény, a másodikban a kilenccel szorzás, a harmadikban a hármat hozzáadás.
Ezt így is meg lehet csinálni:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
- függvényeket tudunk átadni paraméterként, a
map
metódus pl. egyInt=>Int
függvényt vár - lambdákat tudunk deklarálni pl.
{x: Int => 2*x}
alakban, ez ígyInt=>Int
lesz mindenképp, odaadhatjuk amap
nak - de mivel a
map
eleveInt=>Int
függvényt vár, ha nem írjuk kix
explicit típusát, akkor (mivel a mapnak adjuk oda) a fordító arra próbál rá, hogyx
legyenInt
- nem kell kirakjuk a kapcsos köré a kerek zárójelet, mert
map
egy unáris tagfüggvény (elé se kell feltétlen a pontot)
Az alsó három példában láthatjuk, hogy így miért is tudtuk egy füst alatt lekezelni a (valószínűleg) alkalmazásfüggő
,,szorozd konstanssal''-like függvényeket: egyszerűen csak a transzformációt kell átadjuk argumentumként.
Ha ugyanazt a transzformációt használjuk sok helyen a kódban, akkor persze érdemes kimenteni egy (mondjuk) val
mezőbe,
és akkor látszik is, hogy miért is csináljuk, amit - és ha változtatni kell a függvényt, akkor elég lesz egy helyen piszkálni a kódot.
(A fenti példában pl. a myFavFunc
lehet az, amelyiket többször is alkalmazzuk több kollekcióra, és ha így van, akkor
csak egy helyen kell átírjuk, ha valami változik.)
A map
metódus viszont már olyan, amire gyakran lehet szükség, lista- vagy úgy általában kollekcióelemeket transzformálni
gyakran szoktunk, így a map
tagfüggvényként deklarálása teljesen rendben van.
filter¶
Egy másik gyakran használt listaművelet a filter
: pl. ha az IntList
ünkben gondolkodunk,
akkor lehet az egy feladat, hogy legyűjtsük a pozitív értékeket egy listába, vagy a páratlan számokat:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Int
, Boolean
ba képző függvényeket hívják így)
átadásával tudunk egységesen kezelni:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
fold / reduce¶
Egy harmadik tipikus műveletcsalád, amit egy kollekción (mondjuk egy listán) gyakran végzünk: a redukálás, mikor a lista egészéből valamiféle kétváltozós művelet alapján számított eredményt szeretnénk visszaadni.
Ilyenek például:
- adjuk vissza a lista elemeinek az összegét: itt pl. ha a listánk
(x1,x2,..,xn)
, akkor az eredmény azx1+x2+...+xn
összeg, amit az összeadás_+_
kétváltozós műveletből meg tudunk kapni pl.((((x1+x2) + x3) + x4) + ... ) + xn
formában: kiindulvax1
értékéből mint ,,aktuális értékből'', majd a lista minden egyes elemét hozzáadjuk (jobbról) az aktuális értékünkhöz - szorzatát: ugyanez, csak a szorzás művelettel
- maximumát: ugyanez, csak a
max
függvénnyel, minimum is ugyanez
Írjunk olyan függvényt a listánkba, mely kap egy iniciális értéket ,,aktuális érték''ként, és egy kétváltozós
aggregátor függvényt, és a fenti módon összeaggregálja, redukálja a lista elemeit! Ha a függvény neve
mondjuk reduce
, akkor mi lesz a fejléce?
1 |
|
Válasz mutatása
Implementáljuk is a függvényt!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Válasz mutatása
Valójában többféle módon szokás redukálni egy kollekciót:
- A fentit, mikor is a lista elemeinek a típusa egyben a kiszámított eredmény típusa is, azt redukálásnak hívják
- Az asszociativitás egy fontos tulajdonság: azt kéri, hogy az átadott
op
műveletre igaz legyenop(op(x,y),z) == op(x,op(y,z))
tetszőlegesx,y,z
értékekre. Ha ez nem teljesül, akkor tudatosan kell választanunk areduceLeft
és areduceRight
opciók közül: a fenti, amit implementáltunk, areduceLeft
, mikor is a lista fejelemétől kezdjük el az aggregálást, és mindigop(akt,current)
lesz az új aggregált értékünk, ha eddigakt
volt,current
pedig a lista következő eleme. AreduceRight
a lista végétől indul és mindigop(current,akt)
lesz az új érték. - A ,,redukálás'' üres listára nem mindig definiált: sokszor csak a kétváltozós műveletet kell átadjuk kezdőérték nélkül és a lista fejével kerül inicializálásra.
- Ha üres listára, kezdőértékkel kell működjön a kód, és nem feltétlen ugyanaz a típus lesz az aggregált érték,
mint ami a listánkban lévő adatoké, akkor a műveletet
fold
nak nevezzük (és ennek is van left/right változata).
Alakul a listánk! Kár, hogy ha nem Int
eket akarunk tárolni, akkor újra kell írni az egészet... vagy nem?
Kérdések, feladatok¶
- Írjuk meg a
map
ésfilter
metódusokat tail rekurzívan is! Ehhez lehet, hogy szükségünk lesz egyreverse
függvényre, mely megfordítja (tail rekurzívan) a listánkat. - Bizonyítsuk be a korábbi indukciós módszerrel, hogy a map, filter és reduce metódusok fenti implementációja tényleg helyes.