A LISP programozási nyelv

Fájlkezelés, fájl I/O

Bevezetés

A Common Lisp gazdagon el van látva a fájlkezeléshez szükséges könyvtárakkal, függvényekkel. Mi az alapvető fájlkezelési feladatokat fogjuk végigvenni: fájl olvasása, írása, fájlok listázása a fájlrendszerben. A nyelv alapvető fájl I/O eszközei hasonlóan működnek más nyelvekéhez.
A stream absztrakciót segítségével érhetőek el a fájlok írásra és olvasásra, és egy külön úgynevezett "pathnames" absztrakció szolgál a fájlnevek manipulálására, platform független módon. Továbbá, a Common Lisp egyedi funkcionalitásokat biztosít az S-kifejezések (S-expressions) írására és olvasására.

Fájl tartalmának olvasása

A legalapvetőbb fájl I/O feladat egy fájl tartalmának a beolvasása. Filestreamek segítségével olvashatunk egy fájlból, az OPEN függvény segítségével. Alapesetben az OPEN egy karakter-alapú input streammel tér vissza, mely által számos függvény áll rendelkezésünkre a különböző olvasási műveletekhez:

Ha végeztünk a beolvasással, le kell zárni a streamet a CLOSE függvénnyel.
Az OPEN egyetlen kötelező argumentuma a fájl neve, amelyet olvasni szeretnénk. A Common Lisp számos lehetőséget biztosít fájlnevek reprezentálására, de a legegyszerűbb megoldás, ha sztringben adjuk meg a fájl nevét a helyi fájlrendszerben.
Tehát ha feltételezzük, hogy a "valami/fajl/nev.txt" egy fájl, a következőképpen nyithatjuk meg:
(open "/valami/fajl/nev.txt")

Az ettől visszakapott objektumot bármely olvasási függvényhez felhasználhatom. Például egy fájl első sorának a kiiratását az OPEN, READ-LINE, és CLOSE függvények kombinációjával hajthatom végre:
(let ((in (open "/valami/fajl/nev.txt"))) (format t "~a~%" (read-line in)) (close in))

Persze számos probléma adódhat egy fájl olvasása során: Alapesetben az OPEN és a READ függvények hibát jeleznek ilyenkor. Minden függvény testre szabható bizonyos argumentumokkal, hogy hogyan viselkedjen az ilyen hibás esetekben. Ha egy vélhetően nem létező fájlt akarunk megnyitni az OPEN segítségével, és azt akarjuk, hogy ne jelezzen hibát, használhatjuk a következő kulcsszót argumentumként: "if-does-not-exist", manipulálva ezzel az OPEN viselkedését.
A három lehetséges érték: Ezek segítségével az előző példában befolyásolhatjuk, hogy mi történjen, ha a megadott fájl nem létezik:
(let ((in (open "/valami/fajl/nev.txt" :if-does-not-exist nil))) (when in (format t "~a~%" (read-line in)) (close in)))

A három szövegolvasási függvény közül a READ egyedülálló a Lisp-ben, ezt használják Lisp forráskódok olvasására is. Minden alkalommal, amikor meghívják, beolvas egyetlen S-kifejezést, kihagyva a whitespaceket és a kommenteket, majd visszatér az S-kifejezés által jelölt Lisp objektummal.
Példa: ha a "/valami/fajl/nev.txt" fájl tartalma a következő:
(1 2 3) 456 "egy sztring" ;ez egy komment ((a b) (c d))

Ez négy S-kifejezést tartalmaz: A következőképpen olvashatjuk ezeket be:
CL-USER> (defparameter *s* (open "/valami/fajl/nev.txt"))
*S*
CL-USER> (read *s*)
(1 2 3)
CL-USER> (read *s*)
456
CL-USER> (read *s*)
"egy sztring"
CL-USER> (read *s*)
((A B) (C D))
CL-USER> (close *s*)
T

A PRINT függvényt használhatjuk Lisp objektumok "olvasható" formában történő kiírására. Így ha fájlban kell tárolnunk bizonyos adatokat, a PRINT és a READ segítségével egyszerűen megvalósíthatjuk ezt, bonyolult adatformátumok tervezése vagy egyéb plusz feladatok nélkül.

Bináris adatok beolvasása

Alapértelmezés szerint az OPEN karakter streammel tér vissza, amely lefordítja a háttérben levő bájtokat karakterekké egy adott karakter-kódolási séma szerint. Ha nyers bájtokat szeretnénk olvasni, át kell adnunk ezt az OPEN-nek például az "unsigned-byte 8" argumentummal. Az így kapott streamet átadjuk a READ-BYTE függvénynek, mely egy 0 és 255 közötti, integer típusú egészet fog visszaadni minden alkalommal, amikor meghívjuk. A READ-BYTE-nak a karaktert olvasó függvényekhez hasonlóan szintén megadhatók opcionális argumentumok, melyekkel megmondhatjuk, hogyan viselkedjen, ha fájl végét vagy más hibát akar jelezni.

Nagy mennyiségű adatok beolvasása

Egy utolsó beolvasást végző függvény a READ-SEQUENCE, mely karakterekkel és bináris streamekkel is tud dolgozni. Átadunk neki egy sorozatot (általában vektort) és streamet, és ő megpróbálja feltölteni a sorozatot a streamből kinyert adatokkal. A sorozat első olyan elemének indexével tér vissza, amely nem lett hozzáadva, vagy a sorozat hosszával, amennyiben teljesen fel tudta tölteni. Megadhatóak neki a ":start" és ":end" kulcsszavak argumentumként részsorozat meghatározására, hogy csak azt töltse fel.
A szekvencia argumentumnak egy típusnak kell lennie, melynek meg kell felelnie a stream elemtípusának. Mivel a legtöbb operációs rendszer számos formáját támogatja a blokk I/O-nak, a READ-SEQUENCE valószínűleg egy hatékonyabb megoldást biztosít sorozatok előállítására, mintha a READ-BYTE vagy READ-CHAR függvényeket hívnánk meg többször.

Fájlba való kiírás

Ahhoz hogy fájlt tudjunk kiírni, szükségünk lesz egy output stream-re, melyet átadunk az OPEN-nak egy „:direction” kulcsszóval, mely az „:output” argumentuma. Ha megnyitunk egy fájlt írásra, az OPEN feltételezi, hogy az addig nem létezett, és hibával jelzi, ha mégis. Ezt lehetőségünk van beállítani az ":if-exists" kulcsszóval. A ":supersede" argumentummal az OPEN lecseréli a meglévő fájlt, az ":append"-el pedig hozzáfűzi a fájl végéhez a kiírandó tartalmat. Az ":overwrite" olyan streamet ad vissza, mely felülírja a meglévő fájltartalmat az elejétől kezdve. Ha NIL-t adunk argumentumnak, az OPEN a NIL visszatérési érték helyett egy streammel tér vissza, amennyiben a fájl létezik.
A következő példa szemlélteti az OPEN-nel történő kiíratást:

(open "/valami/fajl/nev.txt" :direction :output :if-exists :supersede)

A Common Lisp tehát számos funkciót biztosít adatok írására: Két különböző függvénnyel lehet csak új sort kiírni: Számos funkció kimenete Lisp adat, azaz S-kifejezés: Mivel nem minden objektum írható ki úgy, hogy könnyű legyen visszaolvasni, van egy megoldás, ami ezt biztosítja: *PRINT-READABLY*
A PRINC függvény az eddigiektől eltérően úgy írja ki a Lisp objektumokat, hogy az emberi szemmel könnyen értelmezhető/olvasható legyen. A sztringeket például idézőjelek nélkül írja ki.
Bináris adatok fájlba történő kiírásához az OPEN-t használjuk az „:element-type” argumentummal, hasonlóan, mint olvasásnál az '(unsigned-byte 8). Ezután már írhatjuk is a bájtokat a streamre a WRITE-BYTE segítségével. A nagy mennyiségű adatok kiírására való függvény, a WRITE-SEQUENCE képes bináris adatokkal és karakterekkel is dolgozni. Az írást és olvasást tekintve egy kicsivel a READ-SEQUENCE gyorsabb az írásnál.

Fájl lezárása

A fájlokat fontos lezárni a munka végeztével, mert ez egy igen szűkös erőforrás. Ha fájlkezelés után nem zárunk le egy fájlt, akkor hamarosan rájövünk, hogy nem tudunk megnyitni más fájlokat. Minden OPEN-hez lennie kell egy CLOSE párnak is.
Példa fájlkezelésre:

(let ((stream (open "/valami/fajl/nev.txt"))) ;; valami művelet a streammel (close stream))

Ez a megközelítés azonban két problémát is felvet. Az egyik az, hogy ha lefelejtjük a CLOSE-t, a kód fájlkezelő része hibát fog jelezni minden futásnál. A másik és egyben jelentősebb probléma, hogy nincs garancia arra, hogy a CLOSE lefut. Ha például a kódban a CLOSE előtt van egy RETURN, vagy egy RETURN-FROM, akkor a CLOSE már nem fog lefutni, a stream nem záródik le. Erre a problémára a Common Lisp biztosít egy általános megoldást, mely garantálja, hogy egy kódrészlet biztosan lefusson: ez az UNWIND-PROTECT operátor.
Mivel a tipikus fájlkezelési eljárás (megnyitás, valami művelet a streammel, stream lezárás) elég gyakori, így a nyelv rendelkezik egy WITH-OPEN-FILE makróval az UNWIND-PROTECT–be építve, hogy párosítsa ezeket a mintákat/lépéseket.
Ez alapvetően így néz ki:
(with-open-file (stream-var open-argument*) body-form*)

A WITH-OPEN-FILE gondolkodik arról, hogy a stream a „stream-var” résznél zárva legyen, mielőtt a WITH-OPEN-FILE visszatér Return-nal.
(with-open-file (stream "/valami/fajl/nev.txt") (format t "~a~%" (read-line stream)))

Új fájl létrehozásához a következőt használhatjuk:
(with-open-file (stream "/valami/fajl/nev.txt" :direction :output) (format stream "Szöveg....."))

Fájlnevek

Eddig már használtunk sztringeket fájlnevek reprezentálására. Ugyanakkor a fájlnevek sztringekkel való megadása az aktuális operációs rendszertől, illetve fájlrendszertől függővé teszi a kódunkat. Hasonlóképpen, ha programmal szeretnénk megalkotni a fájlneveket egy bizonyos rendszer szabályai szerint (például „/” jellel elválasztva a könyvárakat), a kódunkat függővé tesszük az aktuális fájlrendszertől.
Ennek elkerülésére a Common Lisp biztosít egy másik megoldást a fájlnevek reprezentálására, a "pathname" (elérési út) objektumokat. A „pathname” objektumok fájlneveket reprezentálnak strukturált formában, melynek segítségével könnyen manipulálhatóak lesznek a fájlnevek, anélkül, hogy a programunkat egyetlen fájlrendszer szintaktikájától függővé tennénk. Az oda-vissza fordítás terhe (a sztringek és a helyi szintaxis között) a Lisp implementációjára hárul. Sajnos a "pathname" absztrakció felvet némi omplikációt. Amikor a "pathname" objektumokat tervezték, az akkoriban elterjedt fájlrendszerek jóval sokszínűbbek voltak, mint a maiak. Ennek következtében ma már kisebb jelentőséggel bír a nyelv ezen szolgáltatása, de ha valaki jól megérti a működésé, igen sokat könnyíthet a fájlnevek előállításában.
Melyik módszert érdemesebb tehát használni, egyszerű sztringekkel elérni egy fájlt, vagy a "pathname" segítségével? Mivel egy felhasználó tudja, hogy Linux vagy Windows rendszert használ, ennek függvényében tudja a fájl elérési útját is, és nem várható el mindenkitől, hogy a részletekig megismerje a Lisp fájlnév kezelési lehetőségeit. A program segítségével generált fájlnevek automatikusan "pathname" objektumok lesznek. A fájlkezelő függvények mindkét megoldást támogatják. Egy az OPEN által visszaadott stream egy fájlnevet reprezentál, azt a fájlnevet mely eredetileg a stream létrehozásánál lett megadva. Ezeket a típusokat együttesen nevezzük fájlnév jelölőknek. Az összes beépített fájlkezelő függvény elfogadja ezeket a típusokat. Azaz az előző példákban is ahol sztringet használtunk a fájlnév megadásához, használhattunk volna pathname objektumot, vagy streamet egyaránt.
A "pathname" egy strukturált objektum, mely egy fájl elérési útját reprezentálja 6 komponens segítségével: host, device, directory, name, type, és version. Ezek mindegyike atomi értékeket jelent, általában sztringeket, egyedül a directory komponens összetettebb, ugyanis ez könyvtárnevek listáját tartalmazza (sztringként), egy „:absolute” vagy „:relative” kulcsszóval bevezetve. Azonban nem mindegyik pathname komponensre van szükség az összes platformon. Egy „névszringet” pathname típusúvá a PATHNAME függvénnyel alakíthatunk át.
Példák:

Minta program fájlkezelés, lista manipulálás

A program beolvassa egy megadott fájl tartalmát egy listába, melyen ezután lehetőségünk van különböző műveleteket végrehajtani: listaelem lecserélése, törlése, levágás a lista elejéről n elemet, a listából kivágni egy szakaszt. Majd az eredményül kapott listát kiírhatjuk egy megadott fájlba. A mintaprogramot itt letöltheti

Szerző neve: Csorba István
Készítés éve: 2012
Használt fejlesztőkörnyezet: GNU Common Lisp 2.6.1