Hlavní navigace

Squeak: návrat do budoucnosti (16)

25. 5. 2004
Doba čtení: 10 minut

Sdílet

Dnes se podíváme na pokročilé prvky jazyka Smalltalk. Zbyla nám řada velmi zajímavých témat - weak kolekce, finalizace objektů, jmenné prostory, sdílené slovníky a environments. Dostaneme se doslova až do střev kompilovaných metod, takže tento díl je jen pro silnější povahy.

Garbage collecting je z pochopitelných důvodů velmi oblíbenou součástí vysokoúrovňových programovacích jazyků. Oceňují ji především programátoři s mladšími nebo fyzicky méně zdatnými sourozenci, kteří jsou zvyklí, že za ně někdo vždycky uklidí. Přináší ovšem některá drobná úskalí, se kterými je třeba se nějakým způsobem vypořádat.

Weak kolekce

K likvidaci objektu garbage collectorem dojde poté, co jej přestanou referencovat jiné objekty. V praxi k tomu nejčastěji dochází v okamžiku ukončení platnosti proměnných, nebo pokud proměnná odkazující na původní objekt začne referencovat jiný objekt – třeba nil

Smalltalk umožňuje ovšem udržovat i reference na objekty, které může GC kdykoliv sprovodit ze světa. K tomu slouží tzv. weak kolekce (WeakArray, WeakSet). Pokud je objekt referencován pouze jimi, může být zrušen.

| array obj |
array := WeakArray ofSize: 1.
array at: 1 put: Object new.
Smalltalk garbageCollect.
obj := array at: 1. " = nil"

Po provedení této sekvence výrazů zůstane v lokální proměnné obj hodnota nil. Výrazem Object new jsme sice vytvořili objekt, ovšem jediný další objekt, který o tomto objektu věděl, byla naše weak kolekce, a té se GC jen zeširoka vysmívá do obličeje.

Pokud bychom neprovedli explicitní garbage collecting, velmi pravděpodobně bychom obdrželi ještě původní objekt, protože GC objekty neruší hned, ale až to sám uzná za vhodné.

Jiná situace nastane v tomto případě:

| array obj |
array := WeakArray ofSize: 1.
obj := array at: 1 put: Object new.
Smalltalk garbageCollect.

Zde je nový objekt stále referencován lokální proměnnou obj (at:put: vrací vkládaný prvek) a tedy k jeho likvidaci nedojde.

Finalizace

Díky automatické správě paměti hrají destruktory (nejen) ve Smalltalku podstatně menší úlohu, než je tomu v C++. Lze se s nimi setkat jen velmi zřídka, nicméně i tak se občas potřebujeme při zrušení objektu postarat např. o korektní zavření souboru, uzavření přístupu k databázi, spuštění autodestrukčního mechanismu našeho hvězdného korábu apod.

Garbage collecting je samozřejmě plně v režii virtuálního stroje. Jenže procesy jsou řešeny na úrovni image Smalltalku a GC si nemůže jen tak z ničeho nic usmyslet, že by mohl spustit ten a ten kód, o kterém ani nemůže s jistotou prohlásit, že se dočká jeho konce. Mělo by to pro konzistenci systému fatální dopad. Proto se GC o finalizaci objektů vůbec nestará.

Přesto ji Smalltalk samozřejmě poskytuje. Je ale vykonávána na úrovni image. Z toho plynou některá omezení, se kterými se ovšem lze setkat i u Javy nebo C#. Není přesně definováno, kdy bude finalizační proces pro rušený objekt vykonán, a není ani zaručeno, že k němu vůbec někdy dojde.

U Javy a C# tomu můžete věřit a nemusíte. Smalltalk je oproti nim mnohem otevřenější a nejenže vám dává možnost do zákulisí finalizace nahlížet, máte samozřejmě možnost ji i přímo ovlivňovat a modifikovat.

Pokud je třeba při rušení objektu provést nějakou akci, stačí u jeho třídy přetížit metodu finalize, např.:

finalize
        self closeDBConnection: self handle.

To ovšem samo o sobě nestačí. Pro správné provedení finalizace objektu je ještě nutno ji zaregistrovat.

| obj |
obj := MyClass new.
MyClass finalizationRegistry add: obj.

Metoda finalizationRe­gistry je implementována v metatřídě třídy Object a vrací registr objetků pro řízené rušení. Při finalizaci se využívá právě weak kolekcí. V systému běží jeden proces (WeakArray runningFinali­zationProcess), který se stará o spouštění destruktorů registrovaných objektů při jejich rušení. Můžete jej v případě potřeby uspat (suspend), zrušit, vytvořit si jiný apod. Pokud není aktivní, ke spuštění žádného finalizačního kódu samozřejmě nedojde.

Postup při popravě objektu je poměrně jednoduchý. Při registraci se odkaz na něj vloží do weak kolekce a asociuje se k němu jeho nově vytvořená mělká kopie (vykonavatel). Když dojde ke schramstnutí objektu garbage collectorem a runningFinali­zationProcess zjistí, že na místě, kde se původně nacházel doširoka se usmívající objekt plný života, se teď mračí ošklivé nil, požádá vykonavatele, aby po původním objektu uklidil (pošle mu zprávu finalize), a za odměnu ho nechá napospas věčně hladovému chřtánu garbage collectoru.

Jmenné prostory

Na začátku výkladu o jmenných prostorech by, myslím, nebylo od věci zmínit, že Squeak žádné nemá. V současné době se vedou bouřlivé diskuse o tom, jestli má smysl je vůbec zavádět. Vypadá to, že zatím vítězí odmítavý postoj. Implementace Smalltalku, které jmenné prostory zavedly, si příliš nepomohly. Náklady na zvýšení složitosti a náročnost prostředí se ani zdaleka nevyrovnají praktickému přínosu.

Nicméně i tak Squeak obsahuje dva prostředky, které poměrně čistým způsobem jmenné prostory nahrazují bez nutnosti dělat zásahy do samotného jazyka. Pravdou ale je, že se v praxi využívají poměrně řídce a uplatnění nacházejí především v okamžiku, kdy už není zbytí. Jsou to sdílené slovníky (pool dictionaries) a prostředí (environments).

Pool dictionaries

Když jsme se bavili o vytváření tříd ve Smalltalku, letmo jsme zmínili, že zprávy pro jejich tvorbu obsahují i parametr jménem poolDictionaries, ale dále jsme to nerozváděli. Jako tento parametr se většinou předává prázdný řetězec.

Obecně se používání sdílených slovníků moc nedoporučuje, protože dokáží neznalému čtenáři vašeho kódu pěkně zamotat hlavu. Je konfrontován s identifikátory velmi pochybného původu, které vlastně jako by vůbec neexistovaly. Nefunguje na nich příkaz print it, není to volání zprávy, nejsou to globální proměnné a vůbec vypadají více než podezřele.

Sdílené slovníky jsou jakousi lokální obdobou globálního slovníku Smalltalk. Jedná se rovněž o slovníky asociací symbolů na objekty. Tyto symboly lze pak v programech použít jako identifikátory.

Sdílené slovníky musí být vytvořeny jako globální proměnné, tzn. musí být odkazovány ze systémového slovníku Smalltalk. Podobně jako u globálních proměnných se doporučuje, aby začínaly velkými písmeny. Jedná se však pouze o doporučení.

Před jejich použitím již musí být inicializovány, tzn. že v okamžiku, kdy překládáme metodu, která využívá některý identifikátor ze sdíleného slovníku, musí být tento slovník již vytvořen a daný symbol obsahovat jako klíč, jinak kompilátor ohlásí chybu. Z toho důvodu se velmi často příkazy pro jejich vytváření vkládají na začátek zdrojových souborů (*.st). Pokud provádíte vyfajlování třídy se sdíleným slovníkem, máte možnost jej nechat uložit také. O zápis instalačních příkazů se v tom případě postará Squeak sám.

Vytvořme si tedy nějaký sdílený slovník. Nejedná se vlastně o nic jiného, než že vytvoříme globální proměnnou třídy Dictionary.

| pool |
pool := Smalltalk
        at: #ODBCConstants
        ifAbsentPut: [ Dictionary new ].

pool at:#BUFFERSIZE put: (1024 * 8).
pool at:#SQLNULLDATA put: -1.
pool at:#SQLSUCCESS put: 0.

Vytvořili jsme si globální proměnnou ODBCConstants a vložili do ní klíče BUFFERSIZE, SQLNULLDATA a SQLSUCCESS Všimněte si, že jsme pro jména klíčů použili velká písmena. Tím bychom měli případného čtenáře našich programů dostatečně upozornit na to, že se jedná o konstanty a nikoli o obyčejné lokální či instanční proměnné.

Ve třídě, kde chceme sdílený slovník použít, pak jeho jméno uvedeme do parametru poolDictionaries.

IdentityDictionary subclass: #ODBCRow
        instanceVariableNames: ''
        classVariableNames: ''
        poolDictionaries: 'ODBCConstants '
        category: 'ODBC-Core'

Ve třídách, které tento sdílený slovník nemají specifikován, identifikátory BUFFERSIZE, SQLNULLDATA a SQLSUCCESS použít nelze. V metodách třídy ODBCRow je ale využívat můžeme např. takto:

columnName := String new: BUFFERSIZE.
sqlReturn == SQLSUCCESS
        ifFalse: ["..."].

Mnohem průhlednější je používat třídní metody (volání by pak vypadalo např. jako ODBCConstants BUFFERSIZE). Jedná se sice o delší, ale čitelnější a operativnější zá­pis.

Více tříd může využívat jeden slovník a naopak jedna třída může využívat i více sdílených slovníků najednou. Vyhledávání v nich pak probíhá v pořadí, v jakém byly do třídy vloženy. Je nutné si dávat pozor na to, že při rekompilaci tříd se dělají pouze nezbytné zásahy. Máte-li dva slovníky pool1 a pool2 obsahující stejné klíče a uvedete-li nejdříve do definice třídy pouze ‚pool2‘ a tento zápis později upravíte na ‚pool1 pool2‘, bude jako první prohledán slovníkpool2. Jinak tomu ale bude v okamžiku, kdy tuto třídu vyfajlujete a nahrajete do jiné image. Nově se totiž vytvoří v pořadí, v jakém jsou zapsány v definici, takže se třída bude ve výsledku chovat jinak. Můžeme tomu říkat vlastnost, můžeme tomu říkat chyba, ale pravdou je, že k této situaci v praxi může dojít s pravděpodobností pouze 1:18183323000, což je shodou okolností telefonní číslo na Alana C. Kaye, duchovního otce Smalltalku.

Sdílené slovníky se nejčastěji používají pro ukládání různých konstant. Ve Squeaku lze nalézt např. sdílený slovník TextConstants obsahující např. kódy kláves, identifikátory pro speciální znaky atd.

Jak souvisejí sdílené slovníky se jmennými prostory? Vyhledávání ve sdílených slovnících probíhá totiž dříve než vyhledávání v globálním slovníku Smalltalk. Není proto žádný problém překrýt pro danou třídu globální proměnnou jiným objektem a tuto vazbu dynamicky měnit.

Děláte-li např. projekt jménem NotJetACoolName a toto jméno používáte jako prefix pro vaše třídy, můžete mít např. třídyNotJetACo­olNameLine a NotJetACoolNa­mePolygon. Když vykreslujete váš polygon, potřebujete vytvářet úsečky např. takto:

line :=  NotJetACoolNameLine from: 0@0 to: 100@200.

Rádi byste používali jen identifikátor Line, ale třída tohoto jména již ve Squeaku existuje. Můžete si ale ve sdíleném slovníku pro třídu NotJetACoolNa­mePolygon vytvořit asociaci#Line->NotJetACoolNa­meLine a pak už vám nic nebrání v jejich metodách používat příkaz

line :=  Line from: 0@0 to: 100@200.

Vytvořený objekt nebude třídy Line, ale třídy NotJetACoolNa­meLine. Výhodou je, že tuto vazbu můžete dynamicky měnit a obejdete se bez zásahů do zdrojových kódů. Podobných konstrukcí byste se měli raději vyvarovat, ale příležitostně se hodit mohou.

Environments

Prostředí poskytují podobné chování jako sdílené slovníky. Jejich možnosti však ještě dále rozšiřují. Setkat se s nimi také můžete jen zřídka, ale alespoň tušit o jejich existenci se určitě vyplatí. Jedná se rovněž o obdoby systémového slovníku a dokonce z něj i dědí. Každá třída si uchovává referenci na prostředí, ve kterém byla kompilována. Hlavní předností prostředí je, že dokáží izolovat množiny změn.

V praxi to vypadá tak, že si vytvoříte nový projekt (world menu – projects – create new morphic project), změníte mu jméno (menu okna projektu – change title) a izolujete množinu změn (changes – isolate changes of this project).

Od tohoto okamžiku jsou veškeré změny v metodách od ostatních projektů izolovány. To znamená, že můžete bez zábran provádět úpravy třeba i v nejzáklad­nějších systémových třídách, ale v okamžiku, kdy váš izolovaný projekt opustíte, bude vše při starém.

Stejně jako jsou projekty hierarchicky uspořádány, jsou hierarchicky uspořádány i izolovaná prostředí, takže izolovaný projekt může obsahovat další izolovaný projekt, který jeho definice dále nezávisle upravuje apod. Změny je možné nechat aplikovat na rodičovský projekt.

Izolování změn se týká pouze metod. Změny v definicích tříd všechny projekty sdílejí. Aktuální prostředí získáte výrazem

Project current environment

Mechanismus v pozadí

Mechanismy, které se skrývají za sdílenými slovníky a prostředími, se mohou na první pohled zdát komplikované, ale ve skutečnost se obejdou bez jakýchkoliv zásahů do virtuálního stroje (je jen třeba lehce upravit kompilátor). Dokonce se bez nich lze obejít a stejná kouzla, jaká poskytují s identifikátory, můžete snadno udělat i ručně.

Vytvořme si např. metodu MyClass a nadefinujme si u ní metodu doIt, která vrátí referenci na třídu kompilátoru.

OObject subclass: #MyClass
        instanceVariableNames: ''
        classVariableNames: ''
        poolDictionaries: ''
        category: 'MyClasses'

doIt
        ^ Compiler

Ke zkompilované metodě se dostaneme výrazem

MyClass methodDictionary at: #doIt

Pustíme-li si nad ním Inspector (Alt+i), uvidíme strukturu fyzické reprezentace metody. Kromě jednotlivých bytekódů obsahuje i pole literálů (z bytekódu jsou literály odkazovány jako index do tohoto pole)

(MyClass methodDictionary at: #doIt) literals

Pole literálů tvoří asociace třídy ReadOnlyVaria­bleBinding mající klíč (key) a hodnotu (value). Tato třída stojí v pozadí toho, proč kdysi tak oblíbený příkaz

Smalltalk := nil

který přiměl smalltalkovskou image, aby s pocitem dobře vykonané práce zrušila sama sebe, již nejde spustit. Ale zoufat nemusíte, neméně užitečný výraz

Processor := nil

stále spustit lze.

Ale zpět k naší kompilované metodě. Asociace literálu obsahuje jako klíč symbol #Compiler a jako hodnotu referenci na tříduCompiler. Tuto asociaci lze ale zaměnit např. tak, aby literál #Compiler odkazoval na zcela jiný objekt, např. třídu Morph.

Výsledkem výrazu

root_podpora

MyClass new doIt

potom není třída Compiler, ale třída Morph. Asociaci literálů bychom podobně mohli libovolně měnit a naprosto neomezeně s nimi manipulovat. Tím jsme ale ještě nevyřešili dynamické měnění této asociace při změně asociace ve sdíleném slovníku nebo prostředí (asociace se nastavují jen jednou při kompilaci metody). Řešení je velmi prosté. V poli literálů nebude asociace jako taková, ale přímo reference na asociaci ve sdíleném slovníku nebo prostředí (slovníky jsou množiny asociací). Jen poměrně jednoduchou úpravou kompilátoru je tak dosaženo velmi zajímavého chování bez nutnosti zasahovat do mechanismu vyhledávání literálů přímo v interpretu virtuálního stroje.

Tuto problematiku jsem zmínil jen pro úplnost výkladu základů Smalltalku. Doufám, že podobné nečisté praktiky nebudete využívat v praxi a odkazovat se při tom na tento seriál. Na druhou stranu to ukazuje, že Smalltalk toho před vámi skrývá jen velmi málo a umožňuje v případě potřeby sám do sebe provádět takové brutální zásahy, o kterých se jeho věhlasnějším zdegenerovaným dětičkám ani nezdá.

Byl pro vás článek přínosný?