03 - szelekció és a substitution model¶
Szelekció¶
...avagy ,,if''. Láttuk, hogy kifejezéseket többféle módon építhetünk és kapnak típust:
- egy literált ha beírunk, annak adott lesz az értéke és a típusa is, pl. ha
"Hello Scala"
a literálunk, az egy String típusú érték, egyben kifejezés; ha42
, akkorInt
típusú érték; hafalse
, akkorBoolean
típusú - és alkalmazhatunk függvényeket (beépítettet vagy sem), ha az argumentumok típusa megfelel a függvény fejlécének, akkor a kifejezés ,,well-formed'' és típusa a függvény kimeneti típusa
Egy további lehetőség az if-else konstrukció: ha B
egy Boolean
típusú kifejezés és E1
, E2
pedig valami
T
típusú kifejezések, akkor if( B ) E1 else E2
is egy T
típusú kifejezés.
Intuitíve nagyon hasonló történik, mint egy imperatív nyelvben: ha B
igaz, akkor a kifejezés értéke
az E1
kifejezés értéke, ha pedig hamis, akkor az E2
kifejezés értéke lesz.
De hogy ez pontosabban mit jelent, ahhoz meg kell ismerjük az operatív szemantikát, ami ,,matematikailag'' definiálja, hogy mi is az, hogy ,,kiértékelés''.
Operatív szemantika: ▹¶
Mikor ,,kiértékelünk'' egy értéket, akkor formailag egyszerű dolgunk van: ha van egy kifejezésünk, az már vagy egy ,,érték'', azaz egy ,,végeredmény'', és akkor már ki van értékelve, vagy pedig egy olyan kifejezés, melynek a kiértékelését még nem fejeztük be.
A kiértékelésben egy lépés végrehajtásának a jele a ▹ lesz, mert ez szokott lenni sok helyen, az operatív
háromszög. Például, ha a kifejezésünk a 3*(1+2)
, akkor egy lépéssel később az 1+2
rész-kifejezést fogjuk
kiértékelni 3
-má, más nem változik, és kapjuk tehát, hogy egy lépéssel később hogy állunk:
3*(1+2) ▹ 3*(3)
, amit tovább értékelhetünk: egy szám egy zárójelben az maga a szám, azaz 3*(3) ▹ 3*3
, és
ezek után a 3*3
-at tovább tudjuk értékelni az intek beépített szorzása alapján 3*3 ▹ 9
módon. Így azt kaptuk,
hogy 3*(1+2) ▹ 3*(3) ▹ 3*3 ▹ 9
, és ez a vége a 9
már egy olyan érték, amit nem tudunk tovább átírni,
így ez az, ami a kiértékelés ,,eredménye'', ha ez a kifejezés egy értékadás jobb oldalán van, akkor a bal oldali
néven nevezett értékkel ezt a 9
számot fogjuk jelölni.
Az if-else
kifejezés operatív szemantikájának a szabályai pedig:
if(true) E1 else E2 ▹ E1
- ez azt mondja, hogy ha a feltétel true, akkor dobjunk elE1
-en kívül mindent, és csak azt értékeljük ki tovább, az lesz az eredményif(false) E1 else E2 ▹ E2
- ez meg azt mondja, hogy ha a feltétel false, akkorE2
-vel számoljunk csak tovább, a kifejezés többi részét eldobhatjuk- ha
if(B1) E1 else E2
ésB1 ▹ B2
, akkorif(B1) E1 else E2 ▹ if(B2) E1 else E2
- ez meg azt mondja, hogy ha a feltétel még nincs kiértékelve konkrét igaz/hamisra, de tudunk rajta tovább számolni, akkor számoljuk tovább a feltételt (in particular, a két ágon a kifejezésekhez még ne nyúljunk addig, amíg a feltétel kiértékelése nincs kész).
Függvények¶
Minden funkcionális nyelven lehet függvényeket deklarálni valamilyen szintaxissal - hiszen a funkcionális azt is jelenti, hogy vannak benne függvények.
Scalában függvényeket a def
kulcsszóval deklarálunk:
1 2 3 |
|
def
kulcsszóval kell kezdeni, utána jön a függvény neve- eztán nyitójelben jönnek a paraméterek,
név : típus
alakban, egyébként vesszőkkel elválasztva, végül egy csukójel - eztán kettősponttal a függvény visszatérési típusa
- majd egy egyenlőségjel, utána meg egy kifejezés, melyben használhatjuk a formális paramétereket is.
- nincs return! a függvény törzse is csak egy kifejezés, ami kiértékelődik valamire és az lesz a ,,visszatérési érték''
- ha több kifejezés van egymás után a függvény törzsében, akkor az ily módon összetett kifejezés értéke az utolsó kifejezés értéke lesz.
Ahogy pedig egy függvényhívást kiértékelünk:
- ha van egy
def f( x1: T1, x2: T2, ..., xn: Tn ) : T = E
függvénydeklarációnk, - és valahol a kódban egy
f( v1, v2, ..., vn )
kifejezésünk, - akkor erre az
f( v1, v2, ..., vn ) ▹ E[x1/v1, x2/v2, ..., x/vn]
átírást alkalmazzuk, - ami azt jelenti, hogy a függvény törzsében az összes
x1
formális argumentumot av1
-re, az összesx2
-tv2
-re stv. cserélünk, kapunk egy (jó hosszú) kifejezést, majd ezt értékeljük tovább.
Call-by-name, call-by-value¶
Fontos megkülönböztetnünk azt, hogy mikor egy f( v1, v2, ..., vn )
alakú kifejezést (,,függvényhívást'') kell kiértékelnünk,
amikor a v1
, v2
stb. nem feltétlenül értékek még, hanem lehetnek még nem kiértékelt kifejezések, tehát pl. további függvényhívások
stb, akkor mit kell tennünk: előbb kiértékelni az összes argumentumot, és csak aztán behelyettesíteni, vagy behelyettesíteni a
kifejezéseket ,,ahogy vannak'' és majd ha odaér a számítás, akkor kiértékelni?
Minden paraméterre külön-külön definiálhatjuk, hogy call-by-name vagy call-by-value legyenek kiértékelve:
- ha az
xi
paraméter call-by-value, akkor előbb ki kell értékeljük az argumentumot, és csak eztán helyettesíthetjük be, - ha pedig call-by-name, akkor magát a kifejezést, amivel hívtuk a függvényt, kiértékelés nélkül kell beelyettesítsük.
Mindkét megközelítésnek vannak előnyei és hátrányai.
Egy példa kiértékelés¶
Nézzük a következő függvényt:
1 2 3 |
|
dup(2)
kifejezést, call-by-value konvencióval (Scalában ez a default), akkor ezt kapjuk:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Scalában ha egy paraméter call-by-name kezelendő, annak a jele a =>
prefix megadása a típus előtt:
1 2 3 |
|
dup(2)
kiértékelése így zajlik:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Egy szokásos példa: a faktoriális függvény¶
Nézzük most akkor példaképp a következő Scala kódot (az object Main extends App
részt mostantól nem írjuk ki, végül is nem
számít, hogy ez a részlet épp egy futtatható App belsejében van vagy máshol):
1 2 3 4 |
|
Kérdések¶
- Próbáljuk meg végigvezetni a fenti faktoriális függvény kiértékelését, ha az argumentumot call-by-name veszi át!
A
println
függvény mindenképp call-by-value kezeli az argumentumát.
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 40 41 42 43 44 45 46 |
|
válasz mutatása
- Próbáljuk meg végigvezetni ugyanezt úgy is, ha a
fact
függvény név szerint kapja meg paraméterét!