Saturday, February 15, 2020

05 - Függvények és a Unit típus

Azért is hívják a Scalát funkcionális programozási nyelvnek (is - meg OOP is), mert a függvények "first-class citizenek" a nyelvben: igaz, hogy a legtöbb imperatív nyelvben is van rá mód, hogy függvényeket adjunk oda egy másik függvénynek argumentumként, de Scalában ez nyelvi szinten úgy támogatott, hogy nagyon egyszerű a szintaxisa.

Láttuk eddig, hogy az Int, Boolean, String stb típusok léteznek Scalában. Ha pedig σ és τ már típusok, akkor σ=>τ az olyan függvények típusa, melyek σ típusú inputot várnak és τ típusú az outputjuk. Pl. egy def strlen( s:String):Int= szignatúrájú függvény típusa String=>Int (a továbbiakban a szövegben összeolvasztom nyíllá ezt a két karaktert: StringInt). És ilyen típusú értékeket is létrehozhatunk, vagy átadhatjuk paraméterként:


def pluszHat(n: Int) = n + 6
println(pluszHat(4)) // prints 10

val f: Int => Int = pluszHat
println(f(4)) //also prints 10

val g = pluszHat _ //inferred type
println(g(4)) //also prints 10

val h = { x: Int => x + 6 }
println(h(4)) //also prints 10

val i: Int => Int = { x => x + 6 }
println(i(4)) //also prints 10

val j: Int => Int = { _ + 6 }
println(j(4)) //also prints 10


Amit fent látunk: van egy függvényünk, az "adj hozzá hatot az inputhoz" függvény, pluszHat a neve, nyilván Int=>Int típusú.
  • Az f értéket úgy hoztuk létre, hogy explicit deklaráltuk a típusát, majd megmondtuk, hogy ez is a pluszHat függvény legyen. Innentől kezdve őt is teljes jogú függvényként kezelhetjük, pl. kiértékelhetjük egy int helyen, mondjuk ezt is 4-re, és az érték 10 lesz, hiszen ez a függvény lényegében egy aliasa a pluszHat függvényünknek.
  • Ötletként felmerülhet, hogy vajon a fordító ki tudja-e következtetni a típusát, ha nem adjuk meg. A val g=pluszHat értékadás fordítási hibát dob, a fordító nem jön rá, hogy most a pluszHatra mint függvény értékként akarunk hivatkozni, nem pedig meghívni valami argumentummal, ezt hiányolja. (Az előbb a környezetből rájött arra, hogy IntInt értékként akarjuk kezelni.) Ezt a speciális wildcard _ karakterrel oldhatjuk fel: ha f egy στ típusú függvény neve, akkor f _ mint kifejezés magát az f függvényt jelöli és στ típusú. Ezért a val g=pluszHat _ értékdeklarációnál már ki tudja következtetni a fordító, hogy akkor a g-nek is IntInt típusú értéknek kell lennie.
  • A további három érték deklarálásánál arra látunk példát, hogy anonim, avagy lambda függvényt pl. hogy adhatunk meg. Mindháromban közös, hogy {} zárójeleken belül adjuk meg a függvényt.
    • A h érték esetében: explicit típusdeklarációval megadtuk, hogy van egy x formális paramétere a függvénynek, ami Int típusú, az értéke pedig az x+6 kifejezés értéke lesz. (Ugyanaz, mintha def sumthin(x:Int)=x+6 deklaráltuk volna a függvényt, azzal, hogy nem kell ezt korábban tegyük, sem nevet kitalálnunk neki. Itt egyértelmű, hogy a kimenet típusa is Int lesz: ha ismert típusú az input, akkor az output típusa kiszámítható, kivéve, ha a függvény rekurzív - de anonim függvény nem lesz rekurzív, hiszen nincs neve, amivel hívni tudná magát.
    • Az i érték esetében: az értéknek deklaráltuk explicit, hogy IntInt típusú legyen, a kapcsoson belül, magában a lambda függvényben nem. De nem baj: a fordító rájön, hogy ha az i-nek egy IntInt típusú függvény az értéke, akkor ott a kapcsosban az az x csak Int lehet. Eztán még leellenőrzi, hogy ekkor a kapcsoson belül deklarált függvény kiértékeléskor tényleg Intet ad-e vissza és ha igen (most igen), akkor rendben, egyébként fordítási hibát kapunk.
    • A harmadik esetben a _ wildcarddal, nélkül deklaráljuk ugyanezt a függvényt. Ha így deklarálunk egy függvényt, az azt jelenti, hogy annyi paraméteres a függvényünk, ahányszor előfordul benne a _ jel, az első paraméter az első _ helyére, a második a másodikéra stb. helyettesítődik be. Most csak egy van, tehát ez egy egyváltozós függvény; mivel j típusa explicit deklarált, ebből kijön, hogy a paraméter Int kell legyen.
 Persze többváltozós függvényeket is tudunk kezelni ilyen módon: ha például a függvényünk maga az összeadás, ugyenezek a módik a következőképp néznek ki:


def plusz(x: Int, y: Int) = x + y
println(plusz(4,6)) // prints 10

val f : (Int,Int) => Int = plusz
println(f(4,6)) //also prints 10

val g = plusz _ //inferred type. csak egy wildcard kell!
println(g(4,6)) //also prints 10

val h = { (x: Int, y: Int) => x + y }
println(h(4,6)) //also prints 10

val i: (Int,Int) => Int = { (x,y) => x + y }
println(i(4,6)) //also prints 10

val j: (Int,Int) => Int = { _ + _ }
println(j(4,6)) //also prints 10

Tehát a függvény formális paraméterlistájának a típus-sorozatát kerek zárójelekbe kell tenni. Erre még visszatérünk, ez a típusok "szorzata" címen fog később futni. Egy pontra érdemes figyelni: a g megadásakor csak egy wildcardra van szükség, ez kell a fordítónak ahhoz, hogy értse, most a függvényről mint (Int,Int)Int értékről akarunk beszélni és nem meghívni akarjuk.

Scalában minden kifejezés, nincs megkülönböztetve "eljárás" és "függvény". Ami egy "eljárás"hoz legközelebb áll, az egy Unit típusú kifejezés. A Unit egy elég kis értéktartományú típus, eddig a Boolean volt a rekorder a kételemű értéktartományával (lehet true vagy false), a Unit ennél kisebb: egyetlen Unit típusú érték van, a () (üres zárójelpár). A println függvény például StringUnit típusú: kap egy Stringet és garantáltan a () értékre értékelődik ki.
Persze ha egy kifejezés típusa Unit, akkor az értékét már ezek szerint kiértékelés nélkül is tudjuk (feltéve, hogy terminál a kiértékelés és nem esik végtelen ciklusba) - egy Unit típusú kifejezés kiértékelése vélhetően azért történik, mert van mellékhatása, amit szeretnénk. Ilyen pl. a println függvény részéről a konzolra kiírás.

Függvényt paraméterként is várhatunk, ezek után talán nem meglepő módon:

  def kiertekelNullaban(f: Int => Int) = f(0)

  println(kiertekelNullaban({ _ + 5 })) //prints 5

  def egymasUtan(f: Int => Int, g: Int => Int): Int => Int = {
    x => g(f(x))
  }

  val megegy_szorketto = egymasUtan({ _+1 }, { _*2 })
  println(megegy_szorketto(3)) //prints (3+1)*2 = 8

  def plusz(x: Int ): Int => Int = {
    y => x + y
  }
  //plusz kap egy x-et és visszaad egy függvényt,
  //ami ha aztán kap egy y-t, akkor visszaadja x+y-t

  val pluszHat = plusz(6)
  println(pluszHat(4)) //prints 10
az első hívásnál  kapunk egy IntInt függvényt, és behelyettesítjük a nullát, visszaadjuk az értéket; a második esetben kapunk két IntInt függvényt, f-et és g-t és visszaadunk egy harmadik függvényt, a kettő kompozícióját: ami egy input x-re visszaadja g(f(x))-et, azaz először alkalmazza rajta f-et, majd az eredményen g-t és ezt az értéket adja vissza. A harmadik esetben pedig egy x számot kapunk, és visszaadjuk azt a függvényt, ami ha megkap egy y számot, akkor adja vissza az x+y értéket. Itt talán érdemes megfigyelni, hogy ez a kétváltozós összeadás függvény "szétbontva" két egyváltozós függvénnyé, vagy mondhatjuk úgy, hogy egy (Int,Int)Int függvényt írtunk át Int(IntInt) alakba. Ezt a műveletet (amit persze akárhány paraméteres függvényre el lehet végezni, végeredményben csupa egyparaméteres függvényt kapva) curryingnek nevezik (és Brooke Haskell Curry után nevezték el így).

No comments:

Post a Comment