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 Intet vagy egy Stringet, 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 Stringet kapott és Unitra é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
val f : String => Unit = println //f egy érték, mégpedig egy String => Unit függvény, konkrétan a println

f("sanyi") //prints sanyi

Ilyenkor persze nem történik semmi "másolás": az 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
f("sanyi")  println("sanyi") // mert f = println volt deklarálva
            ()               // és közben mellékhatásként kiírjuk h sanyi

Felmerül a kérdés, hogy meg lehet-e úszni az explicit típus kiírást:

1
2
val f = println //ez lefordul
f("sanyi")      //ez már nem

Elsőre lehet, hogy nem értjük mi történik, ha föléhúzzuk az egeret ideában az f deklarációjának, azért lehet egy tippünk:

ez egy Unit

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
def plus42( n: Int ) = n+42

val f = plus42       // ez így le se fordul
val g = plus42 _     // így igen, g egy Int=>Int függvény lesz

println( g(3) )      // prints 45

Az első opciót, _ 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
val f = { x: Int => x + 42 }
val g: Int => Int = { x => x + 42 }
val h: Int => Int = { _ + 42 }

Csak azért tesszük ki értékbe mindhármat, hogy ha valaki tesztelni szeretné, írassa ki az 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 egy Int => Int függvényt akarunk deklarálni, tehát az x bejövő paraméter típusa Int kell legyen, és ezek után az x + 42 kifejezés típusa tényleg két int összegének a típusa, azaz Int lesz szintén, mivel ez rendben van, nem kell kiírjuk explicit, hogy x: Int (de ki lehet).
  • a h függvény lambdája annyiban különbözik a g-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
def plus(x: Int, y: Int) = x+y

def p1: (Int,Int) => Int = plus
def p2: (Int,Int) => Int = _ + _
val p3: (Int,Int) => Int = (x,y) => x+y
val p4 = (x:Int, y:Int) => x+y
val p5 = plus _


println( p1(3,4) ) //prints 7
println( p2(3,4) ) //prints 7
println( p3(3,4) ) //prints 7
println( p4(3,4) ) //prints 7
println( p5(3,4) ) //prints 7

Annyi a dolgunk, hogy kerek zárójelben soroljuk fel a bejövő típusokat, vesszővel elválasztva. A 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 plust 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
def forLoop( from: Int, to: Int, what: Int=>Unit ): Unit = {
  if( from <= to ) {
    what(from)
    forLoop(from+1, to, what)
  } else () // redundáns
}

A for ciklusunk most már kap egy függvényt is, amit kiértékel, majd hívja tovább a ciklust az eggyel későbbi indexre, például így:

1
forLoop(1,10,println)

ez így kiírja nekünk a számokat 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 a cond függvény i-re igazat ad-e, ha igen, végrehajtja i-re a what műveletet, majd updateli az i értékét az update 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?


Utolsó frissítés: 2021-02-07 23:06:47