09 - függvény típusok¶
A ,,funkcionális'' programozási nyelvek neve is arra utal, hogy ,,függvények''et lehet bennük nem csak létrehozni,
de first-class citizenként ugyanúgy, mint akár egy Int
et vagy egy String
et, argumentumként másik függvénynek
átadni, vagy akár kifejezés is kiértékelődhet függvénnyé. Erre persze a legtöbb imperatív nyelven is van valamiféle
workaround (C-ben pl. nagyon sokszor használunk callback függvényeket, amiket odaadunk valamilyen másik függvénynek
azzal, hogy ,,és ha végeztél, kérlek hívd meg ezt a másik függvényt''), de egy funkcionális nyelvben alap elvárás,
hogy könnyen lehessen (futásidőben is) létrehozni újabb függvényeket, és nyelvi szinten kényelmesen legyen
támogatva azok használata argumentumként vagy ,,visszatérési'' értékként.
Függvény típus deklarálása¶
Ha van egy T1
típusunk és egy T2
típusunk, akkor Scalában T1 => T2
jelöli az olyan függvények típusát,
melyek T1
típust várnak argumentumként, és T2
típusú lesz az értékük. Pl. ahogy használtuk
a println
függvényt, ami egy String
et kapott és Unit
ra értékelődött ki (mellékhatásként pedig
kiírta a konzolra a kapott argumentumot), az ez alapján egy String => Unit
típusú függvény.
És tényleg, ha akarjuk, deklarálhatunk is egy ilyen típusú értéket és inicializálhatjuk a println
függvénnyel
is akár:
1 2 3 |
|
f
értékbe futás közben a JVM szintjén csak a println függvényre
mutató referencia, kb. egy pointer kerül. Ezután ha kiértékeljük az f("sanyi")
kifejezést,
a következő történik:
1 2 |
|
1 2 |
|
f
deklarációjának, azért
lehet egy tippünk:
A probléma leírásából világos, hogy a fordító szerint ez az f
egy Unit
típusú érték, in particular, még csak
nem is függvény. Miért: azért, mert ez a sor így jelentheti azt is, hogy hívjuk meg a println
függvényt
(recall: van neki 0-változós változata, ami csak kirak egy újsort a konzolra, ez a változat pedig ugyanúgy,
mint a többi println változat, Unit
típusra értékelődik ki, és ezt az értéket adjuk így oda f
-nek).
Aztán így, hogy ez egy Unit
, persze nem lesz értelmezhető egy sorral lejjebb annak, hogy hívjuk meg mint
egy függvényt.
Ha nagyon nem akarunk explicit típusdeklarálni, megtehetjük a függvény neve után egy _
karakter kirakásával,
de ezt most ne a println függvénnyel tegyük, hanem egy sajáttal, pl ami az inputként kapott inthez hozzáad 42-t:
1 2 3 4 5 6 |
|
_
nélkül, nem tudja értelmezni a Scala fordító, arra reklamál, hogy kéne argumentum a plus42
függvénynek.
A println
-nal nem volt ilyen problémája, mert annak volt nulla-változós változata is, így az lefordult.
A másik probléma, ami ilyenkor előjöhet, az, ha a függvénynek több overloadja is van: azért nem a println függvényt használjuk,
mert a println _
miért is pont azt a String => Int
változatot választaná a sok println
opció közül és tényleg: ha
val h = println _
módon adunk értéket, akkor h
a sima nullaváltozós println()
függvényt kapja értékül, azt meg azért
nem fogjuk tudni meghívni.
Lambda függvények¶
Általában ,,lambda'' függvénynek hívják azokat a függvényeket (nem csak funkcionális programozási környezetben), melyeket futásidőben hozunk létre és nincs ,,saját nevük''. Funkcionális nyelvben elvárás, hogy ilyet létrehozni egyszerű legyen, és erre láthatunk is most három példát:
1 2 3 |
|
f(3)
, g(3)
és h(3)
értékeket, mindegyik
45
lesz.
Amit ebből a kódból tanultunk:
- Kapcsos zárójelben meg lehet adni függvényt úgy, hogy kiírjuk az argumentumo(ka)t,
=>
, majd magát a függvény törzsét (akár újabb kapcsosban, ha kell). - Rekurzív függvényt persze nem tudunk így írni, hiszen neve az nem lesz a függvénynek, így a lambda nem fogja tudni meghívni saját magát. (Persze a lambda törzsén belüli scope-ba ha akarunk, defelhetünk egy rekurzív függvényt.)
- Nem kell kiírnunk a bejövő paraméter(ek) típusát akkor, ha a fordító ki tud következtetni egyet: pl. a
g
függvény esetében látja, hogy egyInt => Int
függvényt akarunk deklarálni, tehát azx
bejövő paraméter típusaInt
kell legyen, és ezek után azx + 42
kifejezés típusa tényleg két int összegének a típusa, azazInt
lesz szintén, mivel ez rendben van, nem kell kiírjuk explicit, hogyx: Int
(de ki lehet). - a
h
függvény lambdája annyiban különbözik ag
-től, hogy ha kapcsos zárójelben megadunk egy kifejezést, melyben egy vagy több_
wildcardot használunk, azzal egy annyi változós függvényt definiálunk, ahány_
van a kifejezésben, és (balról jobbra) az első_
helyére kerül behelyettesítésre az első argumentum, a második helyére a második, stb. Így például az{ _ + 42 }
függvény ugyanaz, mint az{ x => x + 42 }
, az{ _+_ }
pedig ugyanaz, mint az{ (x,y) => x+y }
.
Az utolsó két függvény deklarácíóhoz persze kell az, hogy a fordító valahonnan ki tudja következtetni, vajon milyen típusúak kellene legyenek a paraméterek.
Többváltozós függvények¶
Vannak olyan funkcionális nyelvek, illetve matematikai formalizmusok, melyek csak egyváltozós függvényekkel foglalkoznak, lesz később erről is szó, hogy miért. Scalában persze lehet olyan függvény típust is deklarálni és használni, vagy bejövő paraméter típusának, érték típusnak előírni, ami többváltozós (az összeadásnál az előbb is ezt tettük):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
p2
, p3
és p4
deklarációjánál láthatjuk, hogy
adott esetben a függvény körülről elhagyhatjuk a { }
jeleket is, de látni fogunk később olyat is, amikor ez problémát okoz.
Azt is láthatjuk, hogy lehet akár def
, akár val
is bármelyikük, ebben az esetben ez nem okoz lényeges különbséget,
de a def
vs val
kérdéssel nemsokára fogunk foglalkozni kicsit behatóbban. Arra figyeljünk fel, hogy a p5
deklarációjánál
továbbra is csak egy _
wildcard jelöli, hogy most mi a plus
t mint függvényt tekintjuk és nem kiértékelni szeretnénk.
Függvény mint paraméter¶
Visszatérve a korábbi forciklus példára, ott meg tudtuk oldani, hogy n
és m
között printlnoljuk ki a számokat.
Ez egy elég behatárolt use case, de most már meg tudjuk oldani azt is, hogy úgy írjuk meg a for loopot, hogy amit a magjában
kell csinálnunk, az is érkezzen be argumentumként:
1 2 3 4 5 6 |
|
1 |
|
1
-től 10
-ig. Figyeljük meg, hogy most a forLoop
függvény fejléce alapján tudja, hogy a harmadik
argumentumban érkező függvény egy Int=>Unit
függvény kéne legyen, és ezért a println
függvények közül az ilyen szignatúrájút
fogja behelyettesíteni.
Kérdések, feladatok¶
-
Implementáljunk egy olyan
whileLoop( i: Int, cond: Int=>Boolean, update: Int=>Int, what: Int=>Unit): Unit
függvényt, mely (kicsit imperatívan fogalmazva) teszteli, hogy acond
függvényi
-re igazat ad-e, ha igen, végrehajtjai
-re awhat
műveletet, majd updateli azi
értékét azupdate
függvénnyel és ezt ismétli mindaddig, amíg acond
feltétel hamis nem lesz! Ügyeljünk arra, hogy megvalósításunk tail rekurzív legyen. -
Hogyan kellene hívnunk ezt a
whileLoop
függvényt, hogy 10-től 1-ig kiírja a számokat csökkenőben?