Subsections


D. A funkcionális nyelvek néhány sajátossága

sec:mlgraf. fejezetben a hívási gráfok építésének speciálisan funkcionális nyelveknél adódó problémáit és az ezekre javasolt megoldásaimat ismertetem. A fejezetben foglaltak megértéséhez elengedhetetlen a funkcionális nyelvek néhány fontos tulajdonságának ismerete. Azok számára, akik nem járatosak a funkcionális nyelvekben, egy igen rövid ismertető olvasható ebben függelékben.

sec:deklpr. fejezetben röviden elmondtam, amit a funkcionális nyelvekről minimálisan tudni érdemes, legfőképpen, hogy a funkcionális nyelvek két fő jellemzője az érték és a függvényalkalmazás. Azt is megemlítettem, hogy - ellentétben más nyelvekkel - a függvényérték is éppen olyan érték, mint a többi, azzal a többlettel, hogy egy függvényértéket alkalmazhatunk egy másik értékre: ennek eredményeként egy harmadik értéket kapunk (természetesen ez is lehet függvényérték). A továbbiakban ismertetendő tulajdonságok mind erre a két jellemzőre vezethetők vissza.

D.1 Értékek és nevek

Az értékekhez a funkcionális nyelvekben nevet köthetünk. SML-ben ezt az alábbi deklarációval tehetjük meg:

- val myName = "Hanák Dávid";
> val myName = "Hanák Dávid" : string
Az első sorban a `-' jel jelzi, hogy az értelmező bemenetre vár. A val kezdetű paranccsal nevet adunk a "Hanák Dávid" füzérnek, a rendszer ezt a következő sorral nyugtázza, és jelzi, hogy az érték típusa füzér, azaz string. Fontos megérteni, hogy ez nem ugyanaz, mint az imperatív nyelvek változó-deklarációi: itt a név és az általa jelölt érték teljesen ekvivalensek egymással. Ha valahol a nevet használjuk, maga az érték épül be a kifejezésbe, nem pedig hivatkozások láncolata jön létre:
- val HD = myName;
> val HD = "Hanák Dávid" : string
Lehetőség van arra is, hogy a nevet ,,átdefiniáljuk'', azaz egy újabb értékhez kössük. Ennek az újabb értéknek természetesen a típusa is más lehet.
- val myName = pi;
> val myName = 3.14159265359 : real
A pi is egy név, amely a Math könyvtárban van definiálva. Ha most megvizsgáljuk a HD név által jelölt értéket, azt tapasztaljuk, hogy az még mindig a "Hanák Dávid" füzér, tehát valóban az értéket ,,jegyezte meg'', nem a myName nevet:
- HD;
> val it = "Hanák Dávid" : string


D.2 Függvények ,,átnevezése''

Mivel a függvényértékek éppen úgy kezelhetők, mint bármely más érték, a következő teljesen szabályos:

- val f = cos;
> val f = fn : real -> real
- f pi;
> val it = ~1.0 : real
Az első sor hatására az f név ugyanazt a függvényértéket jelöli, mint a cos név (ez utóbbi a Math könyvtárban definiált függvény). A második sor annyit jelent, hogy f egy olyan függvényt jelöl, amely valósból képez valós értéket. A harmadik sorban az f által jelölt függvényértéket alkalmazzuk a már ismert pi értékre, az eredményt a negyedik sor mutatja (az SML `'-vel jelöli a negatív előjelet). A példa alapján is megállapítható, hogy ez a viselkedés teljesen megfelel a matematikában megszokottnak, csupán a ,,szintaktika'' különbözik. (Ott valahogy így fogalmaznánk: ,,Jelölje azt a függvényt, amely...''.)

Hasonló dolgot persze imperatív nyelvek esetében is tehetünk: teljesen szabályos egy olyan C függvény definiálása, amelynek visszatérési értéke és argumentumai megegyeznek valamely más függvényével, és amelynek törzse nem áll másból, mint eme másik függvény meghívásából. Például:

float   f (float x)   { return sin (x); }
A különbség az, hogy ebben az esetben a lefordított változatban egy call utasítás helyett kettő van, és a veremben is létrejön egy újabb hívási szint; hacsak a fordító ki nem optimalizálja. A fenti SML-példában bemutatott eszköz ennél hatékonyabb: f meghívásakor közvetlenül a megfelelő függvényre adódik a vezérlés. A működés kicsit olyan, mint a C-ben a makrók használata, csak nyelvi szinten megoldott, és emiatt sokkal biztonságosabb.

A helyzetet némiképpen bonyolítja, hogy a már egyszer lekötött nevek átdefiniálhatóak. Ha tehát a fenti példa egyenes folytatásaként a következőt adom be az SML-értelmezőnek:

- val f = e;
> val f = 2.71828182846 : real
akkor ettől a ponttól kezdve f nem a cos függvényértéket, hanem az valós értéket jelöli. Ha megpróbálnánk ismét alkalmazni f-et pi-re, akkor az SML-értelmező hibát jelezne.

D.3 Anonim függvények

A funkcionális nyelvek alapja a -kalkulus (lambda-kalkulus), ennek megfelelően az ún. lambda-jelöléssel úgy is definiálhatunk függvényértékeket, hogy nem adunk nekik nevet! (Ezen a módon természetesen nem definiálhatunk rekurzív függvényt, hiszen nem tudunk saját magára hivatkozni.) Ez SML-ben a következőképpen írható le:

- (fn x => x*x) 12;
> val it = 144 : int
A zárójelben lévő kifejezés azt a függvényértéket jelöli, amely az argumentumát négyzetre emeli. Ha ezt alkalmazzuk a 12 értékre, nem meglepő módon 144-et kapunk. Egy ilyen anonim függvénynek (mivel éppen olyan érték, mint a többi) utólagosan nevet is adhatunk:
- val f = fn x => x*x;
> val f = fn : int -> int
Voltaképpen ez a függvények definiálásának kanonikus alakja. Az áttekinthetőbb és ezért gyakrabban használt jelölés csak szintaktikus édesítőszer:
- fun f x = x*x;
> val f = fn : int -> int


D.4 Részlegesen alkalmazható függvények

Az első funkcionális nyelvben, a LISP-ben ez a fogalom még nem létezett. Ott, mint minden mást, egy függvényhívást is egy listával írunk le, amelynek fejében a függvényérték áll, farkában pedig a függvény argumentumai.D.1 Természetesen módunkban áll ezt a listát adatként kezelni, és fokozatosan újabb tagokkal (leendő argumentumokkal) bővíteni, végül a megfelelő helyen az erre alkalmas beépített függvénnyel az egészet meghívni. Az SML - és a többi, a LISP-nél modernebb funkcionális nyelv - erre a ,,fokozatos bővítésre'' sokkal elegánsabb és kényelmesebb lehetőséget nyújt.

Az SML-ben szigorúan véve egy függvénynek pontosan egy argumentuma lehet. Ez az argumentum természetesen tetszőlegesen bonyolult SML-kifejezés lehet, leggyakrabban egy ún. ennes (englishtuple), amely nem más, mint egy n db rögzített helyű mezőből álló rekord, ha úgy tetszik, felsorolás (önmaga is egyetlen érték). Az SML-ben ezt a következőképpen jelöljük:

- val pair = (pi, "pi");
> val pair = (3.14159265359, "pi") : real * string
Itt pair egy olyan párt jelöl, amelynek első tagja a közelítése, második tagja pedig egy füzér. Az ilyen ennesek segítségével lehet egyféleképpen megoldani, hogy egy függvény több argumentumot kapjon. Egy másik lehetőség az ún. részlegesen alkalmazható függvények írása. Vegyük például a következő függvénydefiníciót:
- fun add a b = a+b;
> val add = fn : int -> int -> int
- add 2 3;
> val it = 5 : int
Pontatlanul megfogalmazva mondhatjuk, hogy az add függvénynek két argumentuma van (és a továbbiakban is ezt a fordulatot fogom használni), amelyeket összead, az alkalmazásából ez jól látszik. Valójában azonban a következőről van szó: az add egy részlegesen alkalmazható függvény, amelynek első (pontosabban egyetlen) argumentumát meghatározva (lekötve) egy függvényértéket kapunk eredményül, amelyet egy újabb (ismét egyetlen) argumentumra alkalmazva megkapjuk a végeredményt. Talán könnyebb megérteni, miről is van szó, ha a példa folytatásával rávilágítok a lényegre:
- val addTwo = add 2;
> val addTwo = fn : int -> int
- addTwo 3;
> val it = 5 : int
- addTwo 134;
> val it = 136 : int
Mi is történt itt? Az első sor hatására addTwo azt a függvényt jelöli, amely az argumentumához kettőt ad. Ha ezt háromra alkalmazzuk, ötöt kapunk, ha 134-re, akkor 136-ot. Ha jobban megvizsgáljuk a helyzetet, azt tapasztaljuk, hogy az add név valójában nem is egy, hanem két függvényértéket takar, sőt voltaképpen nagyon sokat, hiszen a részleges alkalmazás eredményeként kapott függvényérték függ add első argumentumától.

Előfordulhatnak olyan esetek is, hogy nem állapítható meg egy függvényről, hogy hányszorosan alkalmazható részlegesen. Pontosabban szólva az argumentumainak száma függhet maguktól az argumentumoktól is. Erre példát és némi magyarázatot aps:fvissza. szakaszban láthatunk.

D.5 Függvényérték mint argumentum

Mivel a függvényérték is érték, semmi akadálya nincs, hogy egy másik függvény argumentumaként szerepeljen. Azokat a függvényeket, amelyek argumentumként egy függvényt várnak, magasabb rendű függvényeknek nevezik. Ilyen például a könyvtári map függvény, amely két argumentumot vár, egy függvényt és egy listát. Az eredményül adott lista hossza megegyezik a második argumentumban átadottéval, és minden eleme olyan érték, amelyet úgy kapunk, hogy az eredeti lista megfelelő elemére alkalmazzuk az argumentumként átadott függvényt. Ezt szemlélteti a következő példa:

- map (fn x => x+1) [1,4,9];
> val it = [2, 5, 10] : int list


D.6 Függvényérték mint visszatérési érték

Arról már volt szó, hogy ha egy függvényt részlegesen alkalmazunk, akkor a visszatérési értéke (eredménye) egy újabb függvény. Ilyen helyzet azonban úgy is előfordulhat, hogy az adott függvény összes argumentumát meghatározzuk. Vegyük a következő példát:

- val f = nth([sin,ln,sqrt], 2);
> val f = fn : real -> real
- f e;
> val it = 1.6487212707 : real
Mi is történt itt? Az nth függvény nem tesz mást, mint visszaadja az első argumentumában átadott lista -edik elemét, ahol a második argumentumban megadott érték. Csakhogy itt most olyan listáról van szó, amelynek elemei a Math könyvtár bizonyos függvényei. (Mivel a függvényérték is érték, ennek nincs semmi akadálya.) Az f érték tehát maga is függvényérték, amelyet alkalmazhatunk egy valós értékre; az így kapott eredmény történetesen négyzetgyöke.


D.7 Lokális kifejezés és deklaráció

Ahogy az imperatív nyelvekben is van lehetőség lokális változók létrehozására és használatára, úgy a funkcionális nyelvek is adnak eszközöket lokális érték-deklarációk használatára. Az értéket eredményező kifejezésnek (pl. pi/2.0) és a deklarációnak (pl. val two = 2) is megvan a lokális érték-deklarációval kiegészített változata. Az előbbit lokális kifejezésnek, az utóbbit lokális deklarációnak nevezik. Lássunk mindkettőre egy-egy példát:

- let val halfPi = pi/2.0 in halfPi end;
> val it = 1.57079632679 : real
- local fun half x = x/2.0 in val halfPi = half pi end;
> val halfPi = 1.57079632679 : real
Az első egy lokális kifejezés, deklarálja a halfPi nevet, amit aztán a kifejezésben felhasznál. A lokális kifejezés értéke természetesen , a halfPi név azonban érvényét veszti az end kulcsszó után. A második egy lokális deklaráció, amely deklarálja a half függvényt, amelyet aztán felhasznál a halfPi név deklarálásakor. Az end kulcsszó után a half függvény nem használható, de a halfPi név igen.D.2

D.8 Infix jelölés

Az SML lehetőséget ad arra, hogy egyes kétoperandusú függvényeket infix alakban használjunk, ha ezt kifejezőbbnek érezzük. Ehhez deklarálni kell, hogy az adott operátor - így nevezzük az infix pozíciójú függvényeket - jobbra vagy balra köt-e, ill. hogy mi a precedenciája. Erre szolgál az infix és az infixr kulcsszó. Ha az infix deklaráció a függvény definiálása előtt helyezkedik el, akkor maga a definíció is infix formájú kell, hogy legyen. Példaként nézzük meg a bináris léptető operátor definícióját!

- infixr 8 <<;
> infixr 8 <<
- fun a << 0 = a
    | a << b = if b > 0 then (2*a) << (b-1)
               else raise Subscript;
> val << = fn : int * int -> int
A 8-as precedenciaszint a szorzásnál is erősebben kötővé teszi az operátort (ezért a zárójel a definícióban), az infixr kulcsszó pedig azt jelzi, hogy jobbra köt. A feltétel else ága hibajelzésre szolgál arra az esetre, ha az operandus negatív lenne.

Az infixként deklarált operátorok továbbra is használhatóak prefix alakban, ha az op kulcsszóval előzzük meg őket, pl. az op«(3,2) jelölés teljesen ekvivalens a 3 « 2 alakkal. Az op kulcsszó használatával azt is megtehetjük, hogy először infixnek deklaráljuk az operátort, majd az ezt követő függvénydefiníciót mégis prefix alakban adjuk meg.

Az infix deklarációk hatását véglegesen is megszüntethetjük a nonfix kulcsszó használatával, ez azonban kerülendő, mert könnyen zavart okozhat.

@deactivate` =

Hanák Dávid <dhanak@inf.bme.hu>