Saturday, February 15, 2020

01 - val, def, típus kikövetkeztetés és a substitution model

A kurzus legnagyobb részében pure funkcionális programozni fogunk (amennyire lehet nagy áldozatok nélkül), ami egyebek közt azt is jelenti, hogy értékekkel (value) fogunk dolgozni, változókkal (variable) pedig csak nagyon elvétve. A kettő között az a különbség, hogy az érték csak egyszer, a deklarálásakor "kap értéket", a változó pedig futás közben mutálódhat, megváltozhat a hozzá rendelt memóriaterületen a tartalom. Ha egy változó / adatszerkezet futás közben nem képes  megváltoztatni a tartalmát (nincsenek mutátorai), akkor őt az immutable, egyébként pedig mutable jelzővel illetjük.

Értéket a val kulcsszóval deklarálunk, majd az érték neve (ez szinte bármi lehet, de azért próbáljunk ésszel nevezni el értékeket és függvényeket), eztán kettőspont, majd az érték típusa, egyenlőség, és végül a kifejezés, aminek az értéke bekerül a megadott nevű értékbe. Például:

object Main extends App {
  val welcome: String = "Hello Scala"
  println(welcome)
}


Ebben a snippetben deklaráltunk egy welcome nevű értéket, aminek a típusa String. Ez a típus megfelel a Java nyelv java.lang.String -ének, konkrétan type String = java.lang.String -ként van deklarálva a Predef nevű objektumban, ami többek közt tartalmazza a gyakran használt típusok aliasait (mint a String) és néhány kényelmi függvényt (mint pl. a println). A Predef objektum minden Scala fordítási egységbe automatikusan importálódik, ezért ezeket a típusokat és függvényeket anélkül látjuk, hogy külön importolnunk kéne bármit.
A létrehozott értéket pedig inicializáltuk a "Hello Scala" értékre.

Ezt követően a következő kifejezés egy println függvényhívás, ami egy Stringet vár és mellékhatásként kiírja a kapott Stringet a konzolra.

Ebben a nyelvben pontosvesszőre szinte soha nem lesz szükségünk: ha egy sorba egyetlen kifejezést írunk (ami ezek szerint például lehet egy érték deklaráció, vagy egy függvényhívás-kifejezés), akkor ennek a végére nem kell írjunk pontosvesszőt. Ha több kifejezést is írunk egy sorba, akkor közéjük rakjunk pontosvesszőt, de a legtöbb esetben nem indokolt soronként több kifejezést is írni.

Nézzünk egy másik példát:

object Main extends App {
  val welcome = "Hello " + "Scala"
  println(welcome)
}

Ha lefuttatjuk, látjuk, hogy ez a kód is ugyanúgy kiírja az üdvözlő szöveget, mint az előző. Két különbség is van az előzőhöz képest:
  • Az értékadó egyenlőség jobb oldalán a "Hello " + "Scala" kifejezés egy összeadás: két Stringet ad össze. (Idézőjelek közt deklarált karaktersorozat a megfelelő String típusú értékké fordul.) Ennek a műveletnek az eredménye egy harmadik String, melyet a két összeadott String egymás után írásával (also called "konkatenációjával") kapunk. A lényeg: egy ilyen kifejezés típusa szintén String lesz.
  • A welcome értéknek nem deklaráltunk explicit típust. Olyan esetekben, amikor egy típus kikövetkeztethető, ez nem kötelező: mivel az értékadás jobb oldalán egy String típusú érték szerepel (és ezt fordítási időben már tudjuk), így ha kihagyjuk a típus megadását, akkor a fordító kikövetkeztet ("infers") neki egyet, jelen esetben a welcome érték típusa tehát String lesz akkor is, ha ezt nem mondtuk meg explicit.
A Stringen kívül még gyakran fogjuk használni az Int, Long, Float, Double, Char és Boolean típusokat: ezek pontosan azok, amiket a nevük sugall, ha akár C-ben, akár Javában programoztunk már. Fordításkor ugyanúgy a JVM (Java Virtual Machine) számára értelmezhető bytecode készül egy Scala forrásfileból, mint a Java esetében, ezért nem is meglepő, hogy ezek a fenti típusok egy az egyben megfelelnek (legalábbis abban, hogy hány biten milyen kódolásban milyen adatot lehet bennük tárolni) a Java ugyanilyen nevű (primitív vagy objektum) típusának: az Int pl. egy 32-bites előjeles szám, a Boolean tartománya meg összesen két értékből áll, az egyik a true, a másik a false.

Függvényeket a def kulcsszóval deklarálhatunk: def, ezt követi a függvény neve, paraméterlistája, kettőspont, a "visszaadott" érték típusa, egyenlőségjel, majd a függvény kifejtése.
A paraméterlista lehet üres (ha a függvény egyáltalán nem vár paramétert), vagy zárójelek közt megadva a bejövő argumentumok listája, köztük vesszővel; egy argumentumot szintén neve, kettőspont, típusa alakban adunk meg. Így:

object Main extends App {
  def addOssze(x: Int, y: Int): Int = {
    x + y
  }
  println(addOssze(2, 3))
}
Ebben a snippetben a példa kedvéért deklaráltunk egy addOssze nevű függvényt, ami két Intet vár paraméterül, és a kimeneti típusa szintén Int; konkrétan, csak összeadja a két argmentumot. Ezek után meg is hívtuk a függvényt az x=2, y=3 értékekkel, és kiírattuk az értékét.
Vegyük észre a különbséget a C / Java nyelvek függvény szintaxisa és eközt: egy dolog a kettőspont után megadott típus, de a másik fontos különbség, hogy nincs return a függvényben! 
Ennek oka a substitution model, ami a Scala nyelvben (feltéve legalábbis, hogy a függvényeink immutable argumentumokat kapnak, ami most teljesül) a "függvényhívás" alakú kifejezések kiértékelésének módja. Egész egyszerűen a következő történik elvi szinten (valójában nem pont ez, azért a Scala fordítómotor hatékonyabb ennél, de biztosak lehetünk abban, hogy ami történik, annak hatása pont ugyanaz lesz, mint a most felvázolt módszerrel lenne): a függvényhíváskor az első formáis paraméter (most az x) helyére behelyettesítjük a függvény törzsében (most a két kapcsos közti részben) az első argumentumot (most a 2-t) mindenhol, a második formális paraméter (y) helyébe a második argumentumot (3), stb, és a függvény törzséből így kapott kifejezéssé írjuk át az eredeti függvényhívásunkat. Vagyis: egy println(addOssze(2, 3)) kifejezés kiértékelésekor első lépésben átíródik println({ 2 + 3 })-ra. Mivel itt a kapcsoson belül már csak egy kifejezés van, a kapcsost eztán elhagyja (ha nagyon formálisak akarunk lenni), átíródik println(2+3)-ra, eztán a 2+3 kifejezést írja át az értékére, println(5), és ezek után a println függvényt (hasonlóan) meghívja, annak kiértékelése közben mellékhatásként kiírodik az 5 a konzolra.
Amiért a kiírásra azt írom, hogy a println függvény hívásának egy "mellékhatása", az azért van, mert nem a kiírás az "értéke" ennek a kifejezésnek! Minden, ami "történik" kifejezés kiértékelés közben a meghívott függvényen "kívül" is látható módon, azt mellékhatásnak nevezzük, így pl a konzolra írás is, egy globális változó átírása is, adatbázisba írás is mellékhatás. A println metódus egyébként Unit típusú - erről a típusról egy későbbi posztban lesz szó, egyelőre felfoghatjuk voidként.

Függvényeknél szintén igaz, hogy ha a fordító ki tudja következtetni az eredmény típusát, akkor nem kell megadnunk, itt pedig ki tudja: az x egy Int, az y egy Int, két Int összegének típusa pedig (Scalában is) egy Int lesz, tehát a függvény fejlécében a : Int rész kihagyható. Továbbá, a függvény törzse körül a kapcsos zárójel szintén elhagyható: ha csak egy kifejezésünk van, nem kell kapcsosba tennünk. Hogy új sorba írjuk-e vagy sem, a fordító számára mindegy:

object Main extends App {
  def addOssze(x: Int, y: Int) = x + y
  println(addOssze(2, 3))
}
is pontosan ugyanarra fordul, mint az előző.
folytköv

No comments:

Post a Comment