Címkearchívumok: interpreter

Programnyelvek és típusok

A Debra programnyelv tervezése közben természetesen előkerült a talán legfontosabb tervezési kérdés: szigorúan típusos, vagy dinamikus legyen a nyelv, esetleg a kettő keveréke? Erről már írtam korábban többször is, most újra összefoglalom a legfontosabb tudnivalókat.

Kétféle programnyelv van: a szigorúan típusos és a dinamikus típusos. Esetleg olyan megoldás is lehet, hogy az egyik típusba tartozó nyelv közeledni kezd a másikhoz és kevert nyelvi konstrukciókat alkalmaz (ilyen megoldás a „dynamic” típus bevezetése a C# nyelvbe).

Szigorúan típusos: deklarálni kell a változók típusát, vagy explicit módon vagy értékadással, ilyenkor az értékből derül ki a típus (nem egyértelműség esetén utótagokkal segíthetünk (L, F, … a C#-ban). Az osztályok mezői is szigorúan deklaráltak. A legfontosabb megkötés az, hogy a változó típusa nem is változhat, a változó élettartama idejére ugyanaz marad, más értéktípust nem rendelhetünk hozzá (kivétel float = int, string = char).

Az ilyen nyelvekben a függvény, eljárás definíciókban is meg kell adni a formális paraméterek típusát, függvények esetében a visszatérési érték típusát is. Ez nehézkesnek tűnhet a dinamikus nyelvekhez képest, de amikor többféle argumentumlistával rendelkező, ugyanolyan nevű függvényekről, eljárásokról van szó, a szigorú típusosság segíti a fordítót abban, hogy kiválassza a legjobban illeszkedő függvényt.

A dinamikus nyelvek legnagyobb előnye, hogy menet közben változhat egy-egy változó típusa, értékadásonként, és nem kell ezt eldönteni a definiálásnál, azaz kevesebb kódot kell írni, a kód rövidebb, áttekinthetőbb lesz. Ugyanakkor ez hátrány is lehet, amikor felüldefiniált (túlterhelt) függvényeket szeretnénk írni, ezt ugyanis dinamikus nyelv esetében nem tehetjük meg, csak akkor, ha különbözik az azonos nevű függvények argumentumszáma. Azonos nevű, egyenlő számú formális paraméterrel rendelkező függvények közül, a fordító nem tud választani, a különböző típusú paramétereket a függvény kódjában, futási időben kell megoldanunk. Egy alternatíva lehet, ha segítjük a fordítót, és adunk típust a formális paramétereknek, de ebben az esetben is futási időben dől el, hogy melyik függvény fog lefutni. Dinamikus nyelvek esetében úgy tűnik, a legjobb megoldás, ha az azonos nevű, azonos számú paraméterrel rendelkező függvények esetében a függvények nevében elhelyezünk valamilyen, az argumentumok típusára utaló jelzést, így a fordító fordítási időben is tud választani az azonos nevű függvények közül.

A legfontosabb különbség a kétféle nyelv között az, hogy szigorúan típusos nyelveknél a típus összeférhetetlenségi hibák már a fordítási időben kiderülnek, a dinamikus nyelveknél ezek a hibák a futás közben jönnek csak elő. Azaz, dinamikus nyelven sokkal könnyebb a programírás, de sokkal nehezebb a hibakeresés. Minden értékadás, függvényhívás egy-egy potenciális hibaforrás, statikus nyelveknél, ez mind előkerül a fordításkor.

Külön meg kell említenünk a tömbök esetét: statikus nyelveknél meg kell mondanunk, hogy a tömb milyen típusú elemeket tartalmaz, és minden tömb csak azonos típusú elemeket tartalmazhat. Dinamikus nyelveknél egy-egy tömb, különböző típusú elemeket is tartalmazhat ugyanabban a tömbben, ami jól jöhet, ha adatbázis rekordokat akarunk beolvasni gyorsan és egyszerűen egy kétdimenziós tömbbe, ahol a sorok olyan tömbök, amiknek az elemei különböző típusúak, az adatbázis egyes mezőinek megfelelően. A tömb feldolgozása viszont futási időben lassabb lehet, és a típusok egyeztetése futási hibákat okozhat a legváratlanabb helyeken.

Statikus nyelvekben is lehet olyan tömböket készíteni, amiknek nem azonos típusúak az elemei, de ekkor elveszítjük a szigorú típusosság minden előnyét.

Foglaljuk most össze, a kétféle nyelv tulajdonságait, különös tekintettel arra, hogyan lehet a kétféle megközelítést keverni, ezzel rugalmasabb nyelvet létrehozni.

Statikus nyelvek:

– minden változónak és függvény argumentumnak definiálni kell a típusát

– könnyen megvalósítható a függvény felüldefiniálás, az azonos nevű függvények közül a fordító választja ki a megfelelőt

– a tömbök csak azonos típusú elemeket tartalmazhatnak, a típust meg kell adni a tömb definiálásánál

– lassabb a kód írása, és az nagyobb is, nehezebben olvasható

– a típusegyezőségi hibák már fordítási időben kiderülnek, de a fordítás lassabb

– jóval kevesebb hiba fordulhat elő futási időben, egyszerűbb a hibakeresés, a futás gyorsabb

– akkor érdemes ilyen nyelvet használni, ha jól definiált a környezet, és a programot sokáig szeretnénk használni

Hogyan lehet dinamikus elemeket bevezetni: a legegyszerűbb, ha bevezetünk egy olyan típust, ami éppen azt jelzi, hogy az ilyen változó bármilyen típusú értéket tartalmazhat (ilyen a „dynamic” is) . A függvények is kaphatnak ilyen argumentumokat, és a tömbök is lehetnek ilyen típusúak, azaz az elemeik bármilyen típusú értéket felvehetnek. Természetesen ezzel elveszítünk minden előnyt, amit a statikus nyelvek biztosítanak, és minden típusellenőrzési művelet, futási időre marad. Nagyon nagy előnye ennek a módszernek, hogy csak akkor vesszük elő, ha tényleg szükség van rá. A fordítóban benne kell lenni a képességnek az ilyen kód kezelésére, de ha nem használjuk, a lefordított programban nem lesznek váratlan típus-kompatibilitási hibák, és a futás is gyors lesz.

Dinamikus nyelvek:

– minden változónak és függvény argumentumnak bármilyen típusú értéke lehet

– a függvény felüldefiniálás csak a különböző argumentumszámú függvények esetében lehetséges, egyébként a névben szerepeltetni kell valamilyen, az argumentumtípusra utaló megkülönböztető jelzést

– a tömbök különböző típusú elemeket is tartalmazhatnak ugyanabban a tömbben is

– gyorsabb a kód írása, az rövidebb, könnyebben olvasható

– a típusegyezőségi hibák csak futási időben derülhetnek ki, a fordítás gyorsabb, de a futás sokkal lassabb

– jóval több hiba fordulhat elő futási időben, nehezebb a hibakeresés

– akkor érdemes ilyen nyelvet használni, ha rugalmas a környezet, és a programot csak alkalomszerűen, esetleg csak egyszer szeretnénk futtatni

Hogyan lehet statikus elemeket bevezetni: a fordítottját kell tennünk annak, amit a statikus nyelveknél elmondtunk. Ha nem adunk meg típust, marad a dinamikus működés, ha a definiálásnál, a függvény argumentumok megadásánál típusnevet is szerepeltetünk, akkor a fordító már tudja ellenőrizni a típus-kompatibilitást fordítási időben is. Ha egy tömbnek határozott típust adunk, akkor a tömb csak ilyen elemeket tartalmazhat. Ha a függvény argumentumok határozott típusúak, akkor máris lehetséges az egyértelmű függvény felülírás.

Következtetés: mindkét típusú programnyelv bővíthető olyan eszközökkel, amelyekkel gyakorlatilag egyenértékű nyelvek hozhatók létre. Ha az elsődleges cél a gyors fejlesztés, akkor legyen a nyelv dinamikus, és egészítsük ki statikus elemekkel. Ha a cél a precízebb, kevesebb futási hibával fenyegető nyelv készítése, akkor az legyen statikus, és tegyünk később bele dinamikus elemeket. Nincs tehát kibékíthetetlen ellentét a kétféle nyelv között.

A Debra nyelv megvalósításához a gyorsabb haladás érdekében a dinamikus változatot fogom használni, ezzel gyorsan tudunk egyszer használatos programokat, prototípusokat, kísérleti programokat készíteni. Majd jöhetnek a statikus definíciók, amivel végül egy teljes értékű nyelv lehet a Debra, ami így már rendelkezni fog mindkét típus előnyeivel, és kiküszöbölheti a hátrányokat is, a programozó dönthet, hogy milyen módszerrel programoz, az eszköz tartalmazni fogja mindkét módszerhez az eszközöket.

Ezzel olyan nyelv jöhet létre, ami egyszerre rugalmas, gyors fejlesztésre alkalmas eszköz, másrészt precíz, hibamentes programozásra is alkalmas lesz.

Athena fotója a Pexels oldaláról

Debra – a kezdetek

Mindig is érdekeltek a programozási nyelvek, főleg a szintaxis. Miért pont ilyenek, mi az előnye vagy a hátránya egy-egy programnyelvnek? Azt már programozói pályafutásom elején láttam, hogy a C nem nekem való, sokkal jobban tetszett a Pascal. Akkoriban a Pascal forrásokat még csupa nagybetűvel írták a könyvekben, arra rögtön rájöttem, hogy kényelmesebb írni és könnyebb olvasni, ha a C-hez hasonlóan kisbetűvel írom a kódot.

És bár a C idegen volt, mindig is irigyeltem a kapcsos zárójeleket a Pascal begin-end blokkja helyett. Így, ahogy volt egy kis időm, a TPA-1148-as számítógép (ezzel dolgoztam akkoriban) Pascal fordítójával elkezdtem egy programot írni, ami egy egyszerűsített forráskódot Pascal-ba fordított át, ezt a programot hívtam MCP-nek. Nem volt ez igazi fordító, egyszerű szöveghelyettesítésekkel és ügyes trükkökkel cserélte a kapcsos zárójeleket begin-end blokkokra, és még néhány számomra kényelmes átalakítást végzett a forrás kódon. Ezekután, nálam a fejlesztés úgy nézett ki, hogy megírtam az „mcp” forrást, átalakítottam Pascal-ra, fordítottam, szerkesztettem és így készült el a végrehajtható program.

TPA-1148 számítógép, hasonlón tanultam programozni, és egy ilyenen telepítettem életemben először operációs rendszert, RSX-11M-et

Problémát ez csak akkor okozott, amikor munkahelyet változtattam, és otthagytam egy csomó „mcp” kódot. A kolléganőmnek, aki programozó matematikus szakon végzett el kellett magyaráznom, hogy miért is használtam az MCP-t, és ha a programjaimon változtatni szeretne, akkor ezt hogyan teheti meg. Persze megoldhattam volna, hogy minden MCP forrást PAS-ra fordítok, és úgy hagyom ott őket, de nem ezt a lehetőséget választottam, meg nem tudom mondani, miért.

Aztán jöttek a 90-es évek, a Dos, a Windows 3.1, a Turbo Pascal, az integrált programozói környezetek, és valahogy már nem volt szükségem ilyen többlépcsős eszközökre, és az MCP-t lassan elfelejtettem.

Aztán jött a fordulat, a megvilágosodás, az interneten böngészve rátaláltam Jack W. Crenshaw „Let’s build a compiler” című 16 részes cikksorozatára, ami a compiler (fordító program) írásról szólt, de nem a már ismert bonyolult, elméleti módon, hanem egészen egyszerű stílusban, és gyakorlati megközelítéssel. Ebben a sorozatban egy egyszerű nyelvet közvetlenül assembly nyelvre fordító program készítésének módját írja le a szerző, és mellékeli a fordító Pascal nyelvű forrását is, ami 68000-es assembly-re fordítja le a forráskódot.

A forrásnyelv egyszerű, egykarakteres változóneveket használ, de az utasításkészlete teljes, és a kifejezései is tartalmazzák a legegyszerűbb, de legfontosabb műveleteket. A cikksorozat egyik legnagyobb erőssége, hogy a lehető legegyszerűbb módon, érthetően magyarázza el a forráskód elemzést, a tokenizálást, és a kifejezés-elemzője is rendkívül egyszerű, de mégis hatékony. És a cikksorozat végén a fordítók mellett az interpreter írásra is kitért a szerző, ami az én szívemet különösen megdobogtatta.

Kiderült, hogy szintaxis-fa (AST), top-down és bottom-up módszerek nélkül is lehet érthető módon tárgyalni a fordítást, és lehet működő compiler-t írni csupán a józan észre támaszkodva, anélkül, hogy az embernek többszáz oldalas elméleten kellene átrágnia magát teljesen feleslegesen, hiszen ezután ugyanúgy nem fog tudni fordítót írni, mint annak előtte.

Ráadásul az ismertetett elemző, tokenizáló módszer lehetővé teszi a contextus-függő nyelvek fordítását is, megoldható például, hogy az „=” jelentsen értékadást, és egyenlőség relációt is attól függően, hogy milyen környezetben van éppen. És igazán ez volt az a pont, amikor elhatároztam, hogy én is belekezdek egy nyelv tervezésébe és egy interpreter, majd egy compiler megírásába.

…folytatás következik…

Debra Interpreter – bemutatkozás

A Debra Interpreter egy általam megtervezett programozási nyelvhez készült interpreter. A közeljövőben a fejlesztésével kapcsolatos írások kerülnek majd ide, illetve letölthetőek és kipróbálhatóak lesznek a program különböző verziói is.

Azért, hogy legyen némi elképzelésünk arról, hogyan is néz ki egy Debra kód, álljon most itt példaként egy konzol program, ami egy szövegállomány sorait számolja meg:

console program LineCount

main {
    if argc() = 0 quit("missing command line argument")
    if not isfile(arg(1)) quit("missing file : " + arg(1))
    i = 0
    f = textin(arg(1))
    while not f.eof() {
        f.read()
        i++
    }
    f.close()
    write "File: ", arg(1), ", lines: ", i
}