A Sather programozási nyelv

Alprogramok



Satherben az eljárások, függvények készítésére közös módszer áll rendelkezésre. A függvény olyan eljárás, amelynek van visszatérési értéke. Ezt a Saher a szignatúrában rögzíti (eleme a szignatúrának, ez is megkülönböztetési alap két eljárás között). Megjegyzés: a továbbiakban vegyesen használjuk ezt a két kifejezést.

Paraméterátadás

Az eljárások argumentumait vesszővel elválasztott listában kell felsorolni. Az argumentumok láthatósága a teljes törzsre kiterjed, elfedi az osztály eljárásait és attribútumait. Ezen kívül minden argumentumnak állítható az átadási iránya is. Alapértelmezés szerint minden bemenő (in) argumentum, egyéb lehetséges értékei: inout, out, once. Az in, inout, out módok értelmezése a szokásos. Bemenő argumentumoknál az aktuális argumentum egy másolatát kapja meg az eljárás, így tehát érték szerinti átadás valósul meg. Az out és inout paraméterek felfoghatók visszatérési értékként is, így több értéket is visszaadhat egy eljárás. Érdekesség, hogy ilyenkor ezt nem csak az eljárás definiálásánál, hanem a meghívásánál is explicit módon jelölni kell. Például:

swap(inout x:INT, inout y:INT) is   tmp:INT := x; x := y; y := tmp; end;   -- használata: swap(inout a[i], inout a[i+1]);     divide(x, y, out dividend, out remainder:INT) is   dividend := x/y;   remainder := x - y*(x/y); end;   -- használata: a:INT := 15; b:INT := 10; div, rem:INT; divide(a, b, out div, out rem); #OUT + "Divident=" + div + " Remainder=" + rem + "\n";

A once mód  pedig csak speciális eljárásoknál, az iterátoroknál használható. Ekkor ezen paraméter kiértékelése csak az első híváskor történik meg.

A paraméterátadási módok jellemzőit az alábbi táblázatban foglalhatjuk össze:

in
     Minden argumentum ‘in’ módú alapértelmezés szerint, ‘in’ kulcsszó nincs. Ilyenkor az aktuális paraméter egy másolata adódik át (érték szerinti paraméterátadás). Ha ez az érték egy referencia, akkor a referencia egy másolata adódik át, tehát a tényleges objektum nem másolódik le, ugyanazt látja a hívó és a hívott eljárás.

out
     Egy ‘out’ argumentum esetén eredmény szerinti paraméterátadás valósul meg: az argumentum a hívott eljárástól adódik át a hívónak, amikor az eljárás befejeződik. A hívott eljárásban egy ‘out’ paraméter értékét csak azután használhatjuk, hogy értéket adtunk neki.

inout
     Egy ‘inout’ argumentum esetén érték-eredmény szerinti paraméterátadás valósul meg: az aktuális paraméter átadódik a hívott eljárásnak (érték szerint, tehát lemásolódik), majd visszaadódik a hívónak amikor az eljárás befejeződik (eredmény szerint). A hívott eljárás által végzett módosítások addig nem lépnek érvénybe az aktuális paraméterre vonatkozóan, amíg az eljárás be nem fejeződik.

once
     Csak iterátoroknál használható (részletesebben ld. az 5.3 részben). Egy ‘once’ módú argumentum pontosan egyszer értékelődik ki, amikor először meghívjuk az iterátort az aktuális ciklusban. Ettől eltekintve a ‘once’ argumentumok pontosan úgy viselkednek, mint az ‘in’ paraméterek, a hívás helyén ezt nem kell jelölni.

Írhatunk olyan rutinokat is, amelyeknek van hagyományos értelemben vett visszatérési értéke is. Például:

Zero:INT is return 0 end;

Attribútum vagy eljárás?

A Sather nyelvben adat és eljárás között a különbség nem is olyan nagy, mint elsőre tűnik. Könnyen adhatunk egy osztálynak olyan jellemzőt, amelyről kívülről nem eldönthető, hogy attribútum vagy eljárás. Minden attribútum definíció új mezőt ad objektum állapotaihoz és létrehoz egy olvasó és egy író műveletet azonos névvel (kivéve konstans attribútumok, melyekhez író eljárás nem készül). Tehát még az értékadás is valójában eljáráshívás. Ennek a szépsége az, hogy később eljárás-párokkal helyettesíthetünk egy attribútumot. (Megjegyzés: erre szükség inkább fejlesztés folyamán előjött implementációs változtatásoknál van, ekkor nem kell az összes hivatkozást is átírni – végül is biztonságosabb, mint a ‘find and replace’.)

Iterátorok

Az iterátorok speciális eljárások, melyeket általában egy halmazt szoktak bejárni. Hasznos eszközök a felhasználói ciklikus léptetések elrejtésére, így sokkal tömörebb, érthetőbb és biztonságosabb kódot kaphatunk. minden osztály definiálhat többféle iterátort is, ezek részei az osztály-interfésznek. Az iterátorok egy objektum-gyűjtemény felett dolgoznak (például fa adatszerkezetre adhatunk pre-, in- és postorder bejáró iterátorokat).

Egy iterátor annyiban különbözik egy eljárástól, hogy egy hívás nem fejeződik be teljesen, újrahíváskor nem elölről indul, hanem folytatódik futása a ‘yield’ állításig, és a paraméterek nem veszítik el értéküket. Ezen kívül meg lehet adni, mely argumentumokat értékeljen ki minden híváskor és melyeket csak a legelső alkalommal. Vannak előre megvalósított iterátorok is: while!, until!, break!, upto!, times!, step!, step_upto!, stb., amelyeket elsősorban ciklusok megfogalmazásához használhatunk. Tekintsünk ezekre néhány példát.

A while! addig iterál, amíg a feltétel igaz, az until! pedig addig, amíg a feltétel igazzá nem válik (vagyis amíg hamis). Ezek minden iterációban kiértékelik a feltételt, és attól függően, hogy igaz vagy hamis, a yield vagy a quit részre futnak. (Egy iterációt a quit segítségével tudunk leállítani.) A break! iterátor pedig arra használható, hogy egy ciklust bármikor leállíthassunk.

sum:INT := 0; i: INT := 0; loop while!(i > 5);   sum := sum + i;   i := i + 1; end; #OUT + "Sum=" + sum + ’\n’;         -- Kiírás: Sum=10

Tekintsünk egy kissé összetettebb példát: a buborékrendezés egy olyan megvalósítását, amelyben a break! iterátorral állítjuk le a külső ciklust ha már végeztünk (persze ebben az esetben könnyen kiküszöbölhetnénk a break! használatát pl. until! iterátorral).

bubble_sort(a:ARRAY{INT}) is   loop                              -- ciklus a break! hívásáig     done: BOOL := true;     i:INT := 0;     loop until!(i = (a.size-2));    -- ciklus, amíg i el nem éri az (a.size-2)-t       if a[i] > a[i+1] then         done := false         swap(inout a[i], inout a[i+1]);       end;       i := i + 1;     end;     if done then break!; end;   end; end;

A times! iterátorral adott lépésszámú ciklust írhatunk:

i:INT := 1; sum:INT := 0; loop 10.times!; sum := sum + i; i := i + 1; end;

Az upto! Iterátorral két egész érték között iterálhatunk végig:

sum:INT:= 0; loop sum := sum+ 10.upto!(20); end;

A step! és a step_upto! iterátorok hasonlóak a times!-hoz, illetve az upto!-hoz, csak egytől különböző lépésköz beállítását is lehetővé teszik. Például az első 10 páratlan szám összege:

sum:INT := 0; loop sum := sum + 1.step!(10,2); end;     sum:INT := 0; loop sum := sum + 1.step_upto!(21,2); end;

Ahogy már korábban is említettük, egy ciklus törzsében tetszőleges helyen és számban szerepelhetnek iterátor hívások. Erre lehet egy jó példa az alábbi kódrészlet:

loop i ::= 1.upto!(100); j ::= 0.step!(50,10); while!(i >= 30); until!(j <= 200); if i = 10 then break! end if; #OUT + "Hello\n"; end;

Kérdés, hogy ez hányszor írja ki a „Hello” szöveget? Az első sor miatt legfeljebb 100-szor, a második miatt legfeljebb 50-szer, a harmadik miatt (tekintve, hogy az i egyesével lépked 1-től) csak 30-szor, a negyedik miatt (mivel a j 0-tól lépked 10-esével) csak 20-szor, azonban az elágazás miatt a 10-edik iterációban kilépünk a ciklusból, így a kiírást végző utasítás csak kilencszer fog lefutni.

Iterátorok definiálása

Mi is definiálhatunk iterátorokat az eljárásokhoz hasonlóan, csak itt meg kell adnunk, hogy mikor és hogyan adódjon vissza a vezérlés a hívó pontra. Eljárás esetén erre csak a return biztosít lehetőséget, amely egyben az eljárás befejeződését is jelenti. Ezzel szemben iterátorok esetén két lehetőségünk is van:

Például az alábbi iterátor egy egész intervallum bejárását valósítja meg:

range!(min, max:INT):INT is   i:INT := min;   loop until!(i > max);     yield i;     i := i + 1;   end; end;

Továbbá nézzük meg például, hogy van megvalósítva az INT osztályban az upto! iterátor:

Upto!(once i:SAME): SAME is   r::=self;   loop until!(r > i);     yield r;     r := r + 1   end end;

Itt látjuk, hogy milyen fontos, hogy csak egyszer értékelődik ki az i, és aztán mindig csak erre hivatkozunk. Ellenben a while! esetén ez semmiképpen nem lenne jó, mert akkor csak olyan ciklusokat tudnánk írni, amelyek vagy egyetlen iterációt sem tesznek meg, vagy végtelen ciklusok lesznek. Azonban itt a paraméter minden híváskor kiértékelődik, tehát az alábbi módon valósíthatnánk meg (ha nem lenne beépített):

while!(pred : BOOL) is   loop     if pred then       yeild     else       quit     end   end end;

Az until esetén csak annyit változtatunk az egészen, hogy a feltételt negáljuk az iterátor törzsében lévő if-ben, a break! pedig rögtön kilép (csak a quit-ból áll), ahogy meghívjuk.

Példa, hogy mennyire tömör kódot lehet írni ennek segítségével (a egy INT-ekből álló tömb):

loop a.set!(a.elt! *2) end;

Closure

Bár Satherben az eljárás nem típus, így eljárás típusú változónk sem lehet, de van valami nagyon hasonló: closure (bezárás vagy inkább lefedés). Egy closure egy rutint vagy egy iterátort zár magába, melynek némely paraméterét szabadon lehet hagyni (‘_’ jellel). Egy closure meghívásakor a nyílt paramétereknek kell csak konkrét értéket megadni

a:ROUT{INT}:INT := bind(3.plus(_))        -- 3-mal növeli a megadott INT értéket b:ITER:INT := bind(3.times!);             -- 3 lépést iterál (0-tól 2-ig) swap(inout x, inout y:INT) is tmp ::= x; x := y; y:=tmp; end; r:ROUT{inout INT, inout INT} := bind(swap(_,_));

Eljárás closure-t ‘call’-lal, míg iterátor lezártat ‘call!’-lal hívhatunk meg. Meghíváskor az aktuális paraméterek a ‘_’ jelek helyére sorban helyettesítődnek.

#OUT + a.call(4);                         -- Kiírás: 7 sum:INT := 0; loop sum := sum + b.call!; end; #OUT + sum;                               -- Kiírás: 3 (0+1+2)

Megjegyzés: out és inout paramétereket nyíltnak kell hagyni, mivel azok értéket adnak vissza.

Írhatunk ilyet is:

plus3:ROUT{INT} := bind(_.plus(3)); #OUT + plus3().call(5);                   -- Kiírás: 8