7. gyakorlat
Imperatív vs. dekleratív programozás¶
Imperatív programozás egy olyan programozási paradigma, amely azt írja le, hogy a program hogyan hajtódik végre. A fejlesztők itt arra összpontosítanak, hogy lépésről lépésre definiálják az eredmény eléréséhez vezető utat, amely esetében a programok utasítások sorrendjéből állnak és végrehajtás sorrendje kulcsfontosságú. Ilyen nyelv például a Java és a C++.
Deklaratív programozás egy olyan programozási paradigma, amely azt írja le, mit kell végrehajtani, nem pedig azt, hogy hogyan kell végrehajtani. A fejlesztők itt az elérni kívánt eredményre fókuszálnak, nem pedig a konkrét lépésekre, amelyekkel azt elérhetik. A deklaratív programozásban azt határozzuk meg, milyen eredményeket várunk el, és a programozási nyelvet arra használjuk, hogy ezek az eredmények létrejöjjenek, anélkül hogy pontosan megadnánk a végrehajtás módját. Ilyen nyelv például a Haskell és Prolog.
A funkcionális programozás egy deklaratív megközelítés, amely egy egyszerű előfeltevésen alapul: programjainkat csak pure függvények felhasználásával építjük fel - más szóval olyan függvényeket írunk, amelyeknek nincs mellékhatása.
Mellékhatásnak számítanak többek között a következők:
-
egy változó módosítása
-
egy adatstruktúra módosítása
-
egy objektum adattagjának módosítása
-
kivétel dobása
-
I/O csatornák használata
Más szóval, ha egy függvénynek nincs más megfigyelhető hatása a program végrehajtására, mint az eredmény kiszámítása a bemeneti paraméterek alapján, akkor azt mondjuk, hogy nincs mellékhatása.
Funkcionális programozás főbb előnyei
- Kifejezőbb (moduláris) programstruktúra
- Kevesebb mellékhatás (kevesebb külső állapot fenntartása)
- Egyszerűbb párhuzamos végrehajtás (nincs versenyhelyzet)
- Könnyebb tesztelhetőség (ugyanarra a bemenetre mindig ugyanaz a kimenet)
Mohó vs. lusta kiértékelés¶
A mohó kiértékelés (szakirodalomban gyakran eager vagy strict kiértékelésként található meg) esetén a kifejezések azonnal kiértékelődnek. amikor azokat egy változóhoz rendeljük. Tehát az összes kifejezés értékét előre kiszámítjuk, mielőtt a program ténylegesen használná őket.
A lusta kiértékelés azt jelenti, hogy a kifejezéseket csak akkor értékeljük ki, amikor ténylegesen szükség van rájuk. Ha nem használunk fel egy változó értékét a programban, akkor annak kiszámítása elmarad, hiába történt meg a hozzárendelés. Ez segít elkerülni a szükségtelen számításokat, amelyek nem befolyásolják a végső eredményt.
Például, képzeljünk el egy végtelen listát: 0, 1, 2, 3, ... , ∞. A feladat, hogy megkeressük az első olyan számot, amelyre igaz, hogy számjegyeinek összege nagyobb, mint 100.
-
Mohó kiértékelés esetén meg kellene határozni az input lista teljes tartalmát először, hogy meghatározza a függvény azt a listaelemet, amelyik megfelel a feltételnek. Mivel a lista végtelen, a program nem képes meghatározni a lista utolsó elemét.
-
Lusta kiértékelés esetén a számokat egyenként értékeljük ki, és csak akkor számítjuk ki a következő számot a listában, ha valóban szükség van rá. Tehát a program az első elemet vizsgálja, és ha az nem felel meg a feltételnek, akkor a következő elemre lép és így tovább. Így sosem kell az összes elemet egyszerre kiszámolni, és a végtelen lista problémája elkerülhető.
Egy másik példa mutatja be a legtöbb programozási nyelvben a logikai operátorok esetén alkalmazott lusta kiértékelését. A lenti kódpélda esetén a b()
függvényhívás csak abban az esetben kerül kiértékelésre, ha az a()
függvényhívás hamis értékkel tért vissza:
1 2 3 |
|
Scala¶
A Scala egy modern, statikusan típusos programozási nyelv, amely ötvözi az objektumorientált és a funkcionális programozás előnyeit. Neve a Scalable Language rövidítéséből ered, mivel kis méretű eszközöktől kezdve egészen nagy, elosztott rendszerekig skálázható. A nyelv támogatja mind a mohó, mind a lusta kiértékelést.
Eredetileg a Java Virtual Machine (JVM) platformra készült, azaz képes együttműködni a meglévő Java könyvtárakkal és eszközökkel, miközben egy modernebb, tömörebb szintaxist kínál. Az utóbbi években a Scala kiterjesztette hatókörét: a Scala.js segítségével JavaScript-re is fordítható, míg a Scala Native lehetővé teszi, hogy gépi kódot generáljunk, teljesen kihagyva a JVM-et.
Scala program fordítására illetve futtatására több lehetőségünk is van CLI-n keresztül:
-
scalac
: a Scala nyelv fordítója, amely közvetlenül fordítja a.scala
fájlokat Java bájtkódra (.class
fájlok). -
sbt
: egy projektmenedzsment eszköz, amely nemcsak a fordítást végzi el, hanem kezeli a Scala projekt függőségeit és a tesztelést is.
A gyakorlat során Scala programjaink fordítását és futtatását az IntelliJ-re bízzuk (lásd fejlesztői környezet, ahol már bekonfiguráltuk a szükséges JDK-t, illetve a Scala plugin telepítését követően az IntelliJ már tartalmazza az sbt-t, ami kezeli a a Scala fordítót is), így nem szükséges manuálisan telepíteni egyéb eszközt.
Egy Scala projekt létrehozásához, a szokásos módon IntelliJ-ben a New Project-et választva tudjuk megtenni, azzal a különbséggel, hogy a bal oldali listából a Scala-t válasszuk ki. Ezen kívül Build system-nek az sbt-t válasszuk és figyeljünk arra is, hogy Scala 3-as verzióját használjuk.
A létrejött projekt több könyvtárat és fájlt is automatikusan legenerál, melyekből a fontosabbak:
-
A project könyvtár az sbt saját konfigurációs mappája, itt találhatók azok a fájlok, amelyek a build rendszer működéséhez szükségesek.
-
A src/main/scala könyvtár tartalmazza a program forráskódját, azaz az alkalmazás működéséhez szükséges Scala fájlokat itt kell létrehozni (a félév során ide fogunk dolgozni).
-
Az src könyvtárban található egy test nevű mappa is, amely például a unit tesztek elhelyezésére szolgál.
-
A target könyvtár az sbt által generált kimeneti mappa, ide kerülnek többek között a fordítás során létrejövő .class fájlok.
-
A build.sbt határozza meg a projekt függőségeit, a Scala verzióját és a projekt nevét, valamint definiálja a build folyamatot az adott projekthez.
Továbbá az IntelliJ IDEA támogatja az interaktív Scala REPL (Read-Eval-Print Loop) használatát is, amely egy parancssori környezet és ennek segítségével Scala kódrészleteket közvetlenül kipróbálhatunk, anélkül hogy külön fájlokat kellene létrehozni vagy lefordítani. Ez különösen hasznos lehet új nyelvi elemek kipróbálásához hibakereséshez vagy funkcionális programozási minták gyors teszteléséhez.
Az IntelliJ-ben a REPL a Tools > Scala REPL menüpontból érhető el (vagy az alapértelmezett CTRL+SHIFT+D billentyűkombinációval). Fontos azonban, hogy a Scala REPL használatához először létre kell hozni egy Scala projektet, és be kell állítani a megfelelő Scala SDK-t az IntelliJ-ben.
A gyakorlatokon a Scala 3-as verzióját használjuk, így minden példa és feladat ehhez a verzióhoz igazodik. A Scala 3 számos újítást vezetett be a Scala 2-höz képest: letisztultabb szintaxist, új függvény- és osztálydefiníciós lehetőségeket, valamint egy fejlettebb típusrendszert, amely támogatja az unió- és metszettípusokat. Emellett jelentősen átalakult a makrókezelés és a mintaillesztés is.
Hello Scala¶
A Scala-ban többféle módon hozhatunk létre osztályokat vagy objektumokat, attól függően, hogy milyen funkcionalitásra van szükségünk:
-
class
: Hagyományos osztály, amely példányosítható (new
kulcsszóval), tartalmazhat konstruktort, adattagokat és metódusokat, azaz állapotot és viselkedést is tárolhat. -
object
: Egy úgynevezett singleton objektum, amely egy osztály egyetlen példányát reprezentálja. Nem példányosítható, és leggyakrabban statikus viselkedést, pl. a program belépési pontját (main
) tartalmazza. Ha egy class-hoz statikus adattagokat és metódusokat szeretnénk hozzáadni a Scala-ban, akkor nem a hagyományos static kulcsszóval, hanem egy object definícióval kell ezt megtenni.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Nyelvi tulajdonságok
- Metódust a def kulcsszóval tudunk létrehozni.
- A Unit egy speciális visszatérési típus Scala-ban, amely azt jelenti, hogy egy függvény nem ad vissza értéket (ez volt a void Java-ban, itt viszont egy tényleges típus, azaz egy változó Unit típusú is lehet).
- A var kulcsszóval úgynevezett mutable változót, míg a val kulcszóval immutable változót hozhatunk létre.
- Az immutable változó értéke a kezdeti értékadást követően nem módosítható. A metódusok paraméterei alapértelmezetten immutable változók Scala-ban.
- Scala-ban a class definíciójában megadott paraméterek automatikusan konstruktorparaméterek lesznek. Ha a val vagy var kulcsszó is szerepel a megadott paramétereknél, akkor ezek az osztályon kívülről is - mint az osztály adattagjai - elérhetőek lesznek, egyébként csak a konstruktor szintjén elérhető immutable változóként használhatóak az osztály definíciójában.
- Ezen felül lehetőség van explicit módon további adattagok és konstruktorok létrehozására is.
- Az alapértelmezés láthatóság public (de létezik a nyelvben protected, private és package-private láthatóság is).
-
case class
: Egy speciális osztály, amely automatikusan generálja azequals
,hashCode
,toString
,copy
, és a mintaillesztéshez szükségesunapply
metódusokat. A mezői alapértelmezetten val-ként viselkednek, és az osztály példányosításanew
kulcsszó nélkül is működik. Kifejezetten hasznos adatmodellekhez és pattern matching-hez. -
case object
: Azobject
-hez hasonlóan singleton objektum, és acase class
-hoz hasonlóan automatikusan generál bizonyos metódusokat, így kényelmesen használható mintaillesztéshez. A simaobject
nem generál automatikusan ilyen metódusokat, míg acase class
-bóé több objektum is példányosítható.
Mintaillesztés
- Mintaillesztéssel - és így a
case object
és acase class
használatával - egy későbbi órán foglalkozunk.
OOP Scala¶
A Scala-ban két fő eszköz létezik az öröklődés és a kód újrafelhasználhatóságának kezelésére: az abstract class
és a trait
.
Nincs interface kulcsszó, helyette a trait használható, amely viselkedést ad hozzá osztályokhoz. A trait-ek nem tartalmaznak állapotot, tehát nem lehet bennük mutable adattag (de immutable adattagot tartalmazhat!). Csak metódusokat és azok implementációját tartalmazhatják, de tartalmazhatnak nem implementált metódusokat is.
Egy osztály egyszerre több trait-et is örökölhet, így lehetővé válik, hogy egy osztály több különböző viselkedést is megvalósítson. Ez a Scala egyik erőssége, és lehetővé teszi az ún. mixin öröklődést, amely különböző forrásokból származó viselkedések kombinálását teszi lehetővé.
Ezzel szemben az abstract class lehetővé teszi az állapot kezelését, tehát az osztály tartalmazhat mutable adattagokat az absztrakt metódusokon felül. Viszont ez a megközelítés csak egyszeres öröklődést támogat, vagyis egy osztály csak egyetlen abstract class-ból örökölhet.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
override
és final
¶
Az override
kulcsszót akkor használjuk, amikor egy szülőosztály vagy trait által deklarált metódust vagy mezőt felülírunk egy alosztályban. Ez segít biztosítani, hogy a szándékunk valóban a felülírásra vonatkozik, és elkerülhetjük a hibákat (például ha figyelmetlenségből hoztunk létre egy megegyező nevű metódust).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
A final
kulcsszó három különböző célra is használható:
Metódusoknál: Ha egy metódust final-lal jelölünk, akkor azt nem lehet felülírni egy alosztályban. Ez hasznos lehet, ha biztosak akarunk lenni abban, hogy egy adott metódus implementációja nem változhat.
Osztályoknál: Ha egy osztályt final-lal jelölünk, akkor azt nem lehet örökölni. Tehát más osztályok nem tudják ezt az osztályt kiterjeszteni.
Adattagoknál: Ha egy val adattagok final-lal jelülünk, akkor azt nem lehet felülírni egy alosztályban.
Ciklusok Scala-ban¶
Scala támogatja az imperatív nyelvekből ismert ciklusszerkezeteket, mint a while és for, de a későbbiekben, a funkcionális programozás szemlélete miatt más megoldásokat kell majd használnunk.
1 2 3 4 5 |
|
1 2 3 |
|
Az App trait¶
Az App trait egy speciális Scala trait, amely automatikusan kezeli a main metódust. Ha egy object örökli ezt a trait-et, akkor a törzsében lévő kód úgy kerül lefuttatásra, mintha a main metódusban lenne. Fontos, hogy más trait-ek nem viselkednek így.
1 2 3 4 5 6 7 8 |
|
Pure funkcionális programozás Scala-val¶
A gyakorlat során pure funkcionális programok írása a cél, ezért az imperatív programozásból megismert megközelítések használata nem megengedettek.
-
A mutable (azaz a var-ral deklarált) változók helyett használjuk a val kulcsszót.
-
Az imperatív ciklusok (pl. while) használata helyett az iterációkat rekurzióval vagy magasabb rendű függvényekkel oldjuk meg.
-
A funkcionális programozásban a return használata is mellékhatásosnak tekinthető, mivel explicit módon megszakítja a metódus végrehajtását. Scala-ban a metódusok az utolsó kifejezés értékét automatikusan visszaadják, így a kód tisztább és könnyebben olvasható, tehát a return utasításra nincs szükség.
Gyakorló feladatok¶
Feladat
Készíts egy Employee nevű osztályt, amely rendelkezik a következő adattagokkal: name (String), salary (Int). Az Employee osztály tartalmazzon egy konstruktort, amely inicializálja a két adattagot. Az Employee osztály tartalmazzon egy displayInfo() metódust is, amely kiírja az alkalmazott nevét és alapfizetését.
Készíts egy Manager osztályt, amely az Employee osztályból származik. A Manager osztály rendelkezzen egy új adattaggal: bonus (Int). A Manager osztály írja felül a displayInfo() metódust.
Hozz létre egy Workable nevű trait-et, amely tartalmazza a work() metódust. A work() metódus a következőt írja ki a standard output-ra: "Working...". A Manager és az Employee osztályok implementálják a Workable trait-et, és hívják meg a work() metódust a saját displayInfo() metódusaikban.
Készíts egy Company nevű object-et, amely tartalmaz egy addEmployee(Employee, index) metódust. Az objektum egy tömbben tárolja az alkalmazottakat. Az objektumnak legyen egy getEmployeeInfo(index) metódusa is, amely visszaadja az alkalmazott információit. Emellett a Company objektumnak legyen egy fireEmployee(index) metódusa is, amely eltávolít egy alkalmazottat a tömbből.
Az elkészült osztályokat védd le a további felülírástól és származtatástól a final segítségével. Törekedj immutable változók használatára, ahol lehet.
App trait segítségével tesztelt a megoldásodat.